diff --git a/scripts/prepare-fumapress.mjs b/scripts/prepare-fumapress.mjs index 859723154..e2a2b2e88 100644 --- a/scripts/prepare-fumapress.mjs +++ b/scripts/prepare-fumapress.mjs @@ -115,11 +115,33 @@ function cloudflarePattern(value, destination = false) { return value.replace(/:([A-Za-z][A-Za-z0-9_]*)\*/g, destination ? ":splat" : "*"); } -export function renderCloudflareRedirects(docs) { +function slugifyOpenApiSummary(value) { + return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); +} + +export function collectOpenApiRedirectTargets(spec) { + const targets = new Map(); + for (const [apiPath, pathItem] of Object.entries(spec?.paths ?? {})) { + for (const [method, operation] of Object.entries(pathItem)) { + if (!/^(?:get|post|put|patch|delete|head|options)$/i.test(method) || !operation?.summary) continue; + const summarySlug = slugifyOpenApiSummary(operation.summary); + const routePath = apiPath.replaceAll("{", "").replaceAll("}", ""); + targets.set(summarySlug, `/en/api-reference${routePath}/${method.toLowerCase()}`); + } + } + return targets; +} + +export function renderCloudflareRedirects(docs, englishOpenApiSpec) { + const apiTargets = collectOpenApiRedirectTargets(englishOpenApiSpec); const lines = ["/ /en/insight/guide 302"]; for (const redirect of docs.redirects ?? []) { const status = redirect.permanent === false ? 307 : 308; - lines.push(`${cloudflarePattern(redirect.source)} ${cloudflarePattern(redirect.destination, true)} ${status}`); + const destinationSlug = redirect.destination.match(/^\/en\/api-reference\/[^/]+\/([^/:*]+)$/)?.[1]; + const destination = destinationSlug && apiTargets.get(destinationSlug) + ? apiTargets.get(destinationSlug) + : redirect.destination; + lines.push(`${cloudflarePattern(redirect.source)} ${cloudflarePattern(destination, true)} ${status}`); } return `${lines.join("\n")}\n`; } @@ -137,6 +159,7 @@ export async function prepareFumapress({ root = ROOT, outRoot = root } = {}) { if (openapi.length !== LOCALES.length) { throw new Error(`Expected one nested OpenAPI source per locale, found ${openapi.length}`); } + const englishOpenApi = JSON.parse(await readFile(path.join(root, openapi.find((item) => item.locale === "en").source), "utf8")); const contentRoot = path.join(outRoot, "content"); const publicRoot = path.join(outRoot, "public"); @@ -177,7 +200,7 @@ export async function prepareFumapress({ root = ROOT, outRoot = root } = {}) { } await cp(path.join(root, "robots.txt"), path.join(publicRoot, "robots.txt"), { force: true }); - await writeFile(path.join(publicRoot, "_redirects"), renderCloudflareRedirects(docs)); + await writeFile(path.join(publicRoot, "_redirects"), renderCloudflareRedirects(docs, englishOpenApi)); return { documents: documents.length, diff --git a/tests/fumapress-migration.test.mjs b/tests/fumapress-migration.test.mjs index d1ff3fc8c..087e2a4d8 100644 --- a/tests/fumapress-migration.test.mjs +++ b/tests/fumapress-migration.test.mjs @@ -144,11 +144,14 @@ test("canonical MDX and nested OpenAPI sources are discovered", async () => { test("Cloudflare redirects preserve all Mintlify routes and add root", async () => { const docs = JSON.parse(await readFile(path.join(root, "docs.json"), "utf8")); - const lines = renderCloudflareRedirects(docs).trim().split("\n"); + const spec = JSON.parse(await readFile(path.join(root, "openapi/service-api-en.json"), "utf8")); + const lines = renderCloudflareRedirects(docs, spec).trim().split("\n"); assert.equal(lines.length, docs.redirects.length + 1); assert.equal(lines[0], "/ /en/insight/guide 302"); assert.ok(lines.includes("/README_EN /en/insight/guide 308")); assert.ok(lines.some((line) => line.startsWith("/en/deploy/platforms/* /en/usage/platforms/:splat "))); + assert.ok(lines.includes("/en/api-reference/system/获取系统信息 /en/api-reference/api/v1/system/info/get 308")); + assert.ok(lines.includes("/en/api-reference/model-providers/创建供应商 /en/api-reference/api/v1/provider/providers/post 308")); }); test("Fumapress config wires static localized docs, OpenAPI, and Mintlify support", async () => { diff --git a/tests/fumapress-static-build.test.mjs b/tests/fumapress-static-build.test.mjs index 4815b8da8..baa6324aa 100644 --- a/tests/fumapress-static-build.test.mjs +++ b/tests/fumapress-static-build.test.mjs @@ -37,6 +37,16 @@ test("all 302 canonical localized documents have static HTML", async () => { assert.deepEqual(missing, []); }); +test("every fixed redirect destination resolves to generated output", async () => { + const redirects = (await readFile(path.join(publicRoot, "_redirects"), "utf8")) + .trim().split("\n").map((line) => line.split(/\s+/)); + for (const [source, destination] of redirects) { + if (destination.includes(":splat")) continue; + const target = destination.replace(/^\//, "").replace(/\/$/, ""); + await access(path.join(publicRoot, target, "index.html"), undefined, `${source} -> ${destination}`); + } +}); + test("static routes never repeat their locale prefix", async () => { for (const locale of ["en", "zh", "ja"]) { await assert.rejects(access(path.join(publicRoot, locale, locale)), { code: "ENOENT" });