fix: preserve Mintlify routes and localized navigation

This commit is contained in:
dadachann
2026-09-04 12:31:27 +00:00
parent 289ac19a22
commit 8b052d9ab0
7 changed files with 1512 additions and 951 deletions
+32 -2
View File
@@ -27,7 +27,7 @@ function decodeXml(value) {
.replaceAll("'", "'");
}
export function renderAlternateSitemap(sitemap, base = BASE) {
export function renderAlternateSitemap(sitemap, base = BASE, explicitGroups = []) {
const urls = [...sitemap.matchAll(/<loc>([^<]+)<\/loc>/g)].map((match) => decodeXml(match[1]));
const groups = new Map();
for (const url of urls) {
@@ -42,6 +42,8 @@ export function renderAlternateSitemap(sitemap, base = BASE) {
}
}
for (const group of explicitGroups) groups.set(`explicit:${group.en}`, group);
const shared = [...groups.values()]
.filter((group) => LOCALES.every((locale) => group[locale]))
.sort((left, right) => left.en.localeCompare(right.en));
@@ -66,6 +68,33 @@ export function renderAlternateSitemap(sitemap, base = BASE) {
return { xml: `${lines.join("\n")}\n`, groups: shared.length };
}
function legacyOpenApiSlug(value) {
return value.replace(/\s+/g, "-").toLowerCase();
}
function encodeRoute(route) {
return route.split("/").map((segment) => encodeURIComponent(segment)).join("/");
}
async function collectOpenApiAlternateGroups(root = ROOT, base = BASE) {
const groups = new Map();
for (const locale of LOCALES) {
const spec = JSON.parse(await readFile(path.join(root, `openapi/service-api-${locale}.json`), "utf8"));
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 tag = operation.tags?.[0] ?? "unknown";
const route = encodeRoute(`api-reference/${legacyOpenApiSlug(tag)}/${legacyOpenApiSlug(operation.summary)}`);
const key = `${method.toLowerCase()} ${apiPath}`;
const group = groups.get(key) ?? {};
group[locale] = `${base}/${locale}/${route}`;
groups.set(key, group);
}
}
}
return [...groups.values()];
}
async function loadSource(source) {
if (/^https?:\/\//.test(source)) {
const response = await fetch(source, { headers: { "user-agent": "LangBot docs alternate-sitemap generator" } });
@@ -77,7 +106,8 @@ async function loadSource(source) {
async function main() {
const source = process.env.SITEMAP_SOURCE ?? path.join(ROOT, "dist/public/sitemap.xml");
const { xml, groups } = renderAlternateSitemap(await loadSource(source));
const openApiGroups = await collectOpenApiAlternateGroups();
const { xml, groups } = renderAlternateSitemap(await loadSource(source), BASE, openApiGroups);
const outputs = process.env.SITEMAP_OUTPUT
? [path.resolve(process.env.SITEMAP_OUTPUT)]
: [path.join(ROOT, "dist/public/sitemap-alternates.xml"), path.join(ROOT, "sitemap-alternates.xml")];
+9 -29
View File
@@ -115,33 +115,15 @@ function cloudflarePattern(value, destination = false) {
return value.replace(/:([A-Za-z][A-Za-z0-9_]*)\*/g, destination ? ":splat" : "*");
}
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"];
export function renderCloudflareRedirects(docs) {
const lines = [
"/ /en/insight/guide 302",
"/zh/develop/adapter/discord /zh/develop/adapter/discord/README 308",
"/scripts/README-blog-articles /en/articles 308",
];
for (const redirect of docs.redirects ?? []) {
const status = redirect.permanent === false ? 307 : 308;
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}`);
lines.push(`${cloudflarePattern(redirect.source)} ${cloudflarePattern(redirect.destination, true)} ${status}`);
}
return `${lines.join("\n")}\n`;
}
@@ -159,8 +141,6 @@ 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");
await rm(contentRoot, { recursive: true, force: true });
@@ -200,13 +180,13 @@ 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, englishOpenApi));
await writeFile(path.join(publicRoot, "_redirects"), renderCloudflareRedirects(docs));
return {
documents: documents.length,
fallbackDefaults: 0,
localeOnlyDocuments,
redirects: (docs.redirects?.length ?? 0) + 1,
redirects: (docs.redirects?.length ?? 0) + 3,
locales: [...LOCALES],
openapi,
};