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
+59 -12
View File
@@ -8,7 +8,7 @@ import { update } from "fumadocs-core/source";
import { uiTranslations } from "fumadocs-ui/i18n";
import { defineDocs } from "fumadocs-mdx/macro";
import { createOpenAPI, type OpenAPIServer } from "fumadocs-openapi/server";
import { readdirSync } from "node:fs";
import { readFileSync, readdirSync } from "node:fs";
import path from "node:path";
const SITE_URL = "https://docs.langbot.dev";
@@ -16,17 +16,27 @@ const LOCALES = ["en", "zh", "ja"] as const;
const MINTLIFY_LOCALE = { en: "en", zh: "cn", ja: "jp" } as const;
const HREFLANG = { en: "en", zh: "zh-CN", ja: "ja" } as const;
const mintlifyDocs = readMintlifyDocs();
const translations = defineMintlifyI18n(mintlifyDocs, {
const i18n = defineMintlifyI18n(mintlifyDocs, {
localeMap: { en: "en", cn: "zh", jp: "ja" },
}).translations()
});
const translations = i18n.translations()
.extend(uiTranslations())
.add({
en: { displayName: "English" },
zh: { displayName: "简体中文" },
ja: { displayName: "日本語" },
});
// Fumapress stores translations.config as its runtime i18n config. Preserve
// the Mintlify adapter's locale bridge there so navigation.languages selects
// the matching English, Chinese, or Japanese tree instead of always using en.
Object.assign(translations.config, {
_getMintlifyLanguage: (i18n as typeof i18n & {
_getMintlifyLanguage?: (locale: string) => string | undefined;
})._getMintlifyLanguage,
});
// Locale-only files must not inherit the English storage and create indexed
// pages in locales where no canonical source translation exists.
i18n.fallbackLanguage = null;
translations.config.fallbackLanguage = null;
function collectRouteLocales() {
@@ -70,6 +80,42 @@ const enOpenAPI = createOpenAPI({ input: ["openapi/service-api-en.json"] });
const zhOpenAPI = createOpenAPI({ input: ["openapi/service-api-zh.json"] });
const jaOpenAPI = createOpenAPI({ input: ["openapi/service-api-ja.json"] });
function legacyOpenApiSlug(value: string) {
return value.replace(/\s+/g, "-").toLowerCase();
}
const legacyOpenApiLayout = {
groupBy: "tag" as const,
name: (entry: { info: { title: string } }) => legacyOpenApiSlug(entry.info.title),
};
function encodeRoute(route: string) {
return route.split("/").map((segment) => encodeURIComponent(segment)).join("/");
}
const openApiAlternates = new Map<string, Partial<Record<(typeof LOCALES)[number], string>>>();
for (const locale of LOCALES) {
const spec = JSON.parse(readFileSync(`openapi/service-api-${locale}.json`, "utf8"));
for (const [apiPath, pathItem] of Object.entries(spec.paths ?? {}) as [string, Record<string, any>][]) {
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 alternates = openApiAlternates.get(key) ?? {};
alternates[locale] = route;
openApiAlternates.set(key, alternates);
}
}
}
const openApiAlternatesByRoute = new Map<string, Partial<Record<(typeof LOCALES)[number], string>>>();
for (const alternates of openApiAlternates.values()) {
for (const locale of LOCALES) {
const route = alternates[locale];
if (route) openApiAlternatesByRoute.set(`${locale}:${route}`, alternates);
}
}
function localizeOpenAPISource(
source: Awaited<ReturnType<OpenAPIServer["staticSource"]>>,
locale: string,
@@ -103,18 +149,19 @@ export default defineConfig({
content: {
docs: docs.toFumadocsSource(),
openapiEn: localizeOpenAPISource(
await enOpenAPI.staticSource({ baseDir: "en/api-reference" }),
await enOpenAPI.staticSource({ baseDir: "en/api-reference", ...legacyOpenApiLayout }),
"en",
),
openapiZh: localizeOpenAPISource(
await zhOpenAPI.staticSource({ baseDir: "zh/api-reference" }),
await zhOpenAPI.staticSource({ baseDir: "zh/api-reference", ...legacyOpenApiLayout }),
"zh",
),
openapiJa: localizeOpenAPISource(
await jaOpenAPI.staticSource({ baseDir: "ja/api-reference" }),
await jaOpenAPI.staticSource({ baseDir: "ja/api-reference", ...legacyOpenApiLayout }),
"ja",
),
},
i18n,
translations,
defaultLayoutProps: ({ lang }) => ({
links: localizedNavbar(lang),
@@ -123,10 +170,10 @@ export default defineConfig({
page: (page) => {
const locale = page.locale as (typeof LOCALES)[number];
const route = page.slugs.join("/");
const available = route.startsWith("api-reference/")
? new Set<string>(LOCALES)
: routeLocales.get(route) ?? new Set<string>([locale]);
const alternates = LOCALES.filter((candidate) => available.has(candidate));
const localizedRoutes = route.startsWith("api-reference/")
? openApiAlternatesByRoute.get(`${locale}:${route}`) ?? { [locale]: route }
: Object.fromEntries([...routeLocales.get(route) ?? new Set<string>([locale])].map((candidate) => [candidate, route]));
const alternates = LOCALES.filter((candidate) => localizedRoutes[candidate]);
const defaultLocale = alternates.includes("en") ? "en" : alternates[0];
return <>
<link rel="canonical" href={`${SITE_URL}${page.url}`} />
@@ -135,14 +182,14 @@ export default defineConfig({
key={candidate}
rel="alternate"
hrefLang={HREFLANG[candidate]}
href={`${SITE_URL}/${candidate}/${route}`.replace(/\/$/, "")}
href={`${SITE_URL}/${candidate}/${localizedRoutes[candidate]}`.replace(/\/$/, "")}
/>
))}
{defaultLocale && (
<link
rel="alternate"
hrefLang="x-default"
href={`${SITE_URL}/${defaultLocale}/${route}`.replace(/\/$/, "")}
href={`${SITE_URL}/${defaultLocale}/${localizedRoutes[defaultLocale]}`.replace(/\/$/, "")}
/>
)}
</>;
+32 -2
View File
@@ -27,7 +27,7 @@ function decodeXml(value) {
.replaceAll("&apos;", "'");
}
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,
};
+885 -885
View File
File diff suppressed because it is too large Load Diff
+480
View File
@@ -0,0 +1,480 @@
/en/api-reference/bots/create-a-bot
/en/api-reference/bots/delete-a-bot
/en/api-reference/bots/get-a-bot
/en/api-reference/bots/get-bot-event-logs
/en/api-reference/bots/list-all-bots
/en/api-reference/bots/send-a-message-through-a-bot
/en/api-reference/bots/update-a-bot
/en/api-reference/embedding-models/create-an-embedding-model
/en/api-reference/embedding-models/delete-an-embedding-model
/en/api-reference/embedding-models/get-an-embedding-model
/en/api-reference/embedding-models/list-all-embedding-models
/en/api-reference/embedding-models/test-an-embedding-model
/en/api-reference/embedding-models/update-an-embedding-model
/en/api-reference/files/upload-a-document
/en/api-reference/files/upload-an-image
/en/api-reference/knowledge-bases/add-a-file-to-a-knowledge-base
/en/api-reference/knowledge-bases/create-a-knowledge-base
/en/api-reference/knowledge-bases/delete-a-knowledge-base
/en/api-reference/knowledge-bases/get-a-knowledge-base
/en/api-reference/knowledge-bases/list-all-knowledge-bases
/en/api-reference/knowledge-bases/list-knowledge-base-files
/en/api-reference/knowledge-bases/remove-a-file-from-a-knowledge-base
/en/api-reference/knowledge-bases/retrieve-from-a-knowledge-base
/en/api-reference/knowledge-bases/update-a-knowledge-base
/en/api-reference/knowledge-engines/get-the-knowledge-engine-creation-schema
/en/api-reference/knowledge-engines/get-the-knowledge-engine-retrieval-schema
/en/api-reference/knowledge-engines/list-available-knowledge-engines
/en/api-reference/llm-models/create-an-llm-model
/en/api-reference/llm-models/delete-an-llm-model
/en/api-reference/llm-models/get-an-llm-model
/en/api-reference/llm-models/list-all-llm-models
/en/api-reference/llm-models/test-an-llm-model
/en/api-reference/llm-models/update-an-llm-model
/en/api-reference/model-providers/create-a-model-provider
/en/api-reference/model-providers/delete-a-model-provider
/en/api-reference/model-providers/get-a-model-provider
/en/api-reference/model-providers/list-all-model-providers
/en/api-reference/model-providers/update-a-model-provider
/en/api-reference/parsers/list-available-parsers
/en/api-reference/pipelines/copy-a-pipeline
/en/api-reference/pipelines/create-a-pipeline
/en/api-reference/pipelines/delete-a-pipeline
/en/api-reference/pipelines/get-a-pipeline
/en/api-reference/pipelines/get-pipeline-extension-configuration
/en/api-reference/pipelines/get-pipeline-metadata
/en/api-reference/pipelines/list-all-pipelines
/en/api-reference/pipelines/update-a-pipeline
/en/api-reference/pipelines/update-pipeline-extension-configuration
/en/api-reference/plugins/delete-a-plugin
/en/api-reference/plugins/get-plugin-configuration
/en/api-reference/plugins/get-plugin-details
/en/api-reference/plugins/install-a-plugin-from-github
/en/api-reference/plugins/install-a-plugin-from-marketplace
/en/api-reference/plugins/list-installed-plugins
/en/api-reference/plugins/update-plugin-configuration
/en/api-reference/plugins/upgrade-a-plugin
/en/api-reference/system/get-system-information
/en/api-reference/tools/get-tool-detail
/en/api-reference/tools/list-all-available-tools
/en/articles
/en/articles/connect-deepseek-to-wechat
/en/articles/deploy-ai-bot-in-5-minutes
/en/articles/dify-agent-discord-telegram-slack
/en/articles/dify-langbot-rag-knowledge-base
/en/articles/langbot-cloud-multi-user-workspaces
/en/articles/langbot-plugin-system-deep-dive
/en/articles/langbot-v4100-sandbox-skills
/en/articles/langbot-v490-rag-plugin-architecture
/en/articles/langflow-drag-and-drop-ai-bot
/en/articles/langtars-remote-pc-control-dify-n8n
/en/articles/n8n-multi-platform-ai-chatbot
/en/articles/welcome
/en/deploy/command
/en/deploy/langbot/docker
/en/deploy/langbot/kubernetes
/en/deploy/langbot/manual
/en/deploy/langbot/one-click/1panel
/en/deploy/langbot/one-click/bt
/en/deploy/langbot/package
/en/deploy/settings
/en/deploy/update
/en/develop/agent-context
/en/develop/comp-arch
/en/develop/dev-config
/en/develop/plugin-runtime
/en/insight/community
/en/insight/data-collection-policy
/en/insight/features
/en/insight/guide
/en/insight/platform-features
/en/insight/troubleshooting
/en/plugin/compatibility
/en/plugin/dev/apis/common
/en/plugin/dev/apis/messages
/en/plugin/dev/apis/pipeline-events
/en/plugin/dev/apis/tech-details
/en/plugin/dev/basic-info
/en/plugin/dev/components/add
/en/plugin/dev/components/command
/en/plugin/dev/components/event-listener
/en/plugin/dev/components/knowledge-engine
/en/plugin/dev/components/knowledge-retriever
/en/plugin/dev/components/page
/en/plugin/dev/components/parser
/en/plugin/dev/components/tool
/en/plugin/dev/directory-structure
/en/plugin/dev/migration
/en/plugin/dev/publish/github
/en/plugin/dev/publish/market
/en/plugin/dev/style
/en/plugin/dev/tutor
/en/plugin/plugin-intro
/en/tags/readme
/en/tags/webhook
/en/usage/knowledge/readme
/en/usage/mcp/readme
/en/usage/models/readme
/en/usage/pipelines/coze
/en/usage/pipelines/deerflow
/en/usage/pipelines/dify
/en/usage/pipelines/langflow
/en/usage/pipelines/n8n
/en/usage/pipelines/readme
/en/usage/pipelines/weknora
/en/usage/platforms/dingtalk
/en/usage/platforms/discord
/en/usage/platforms/http-bot
/en/usage/platforms/kook
/en/usage/platforms/lark
/en/usage/platforms/line
/en/usage/platforms/qq/aiocqhttp/lagrange
/en/usage/platforms/qq/aiocqhttp/llonebot
/en/usage/platforms/qq/aiocqhttp/napcat
/en/usage/platforms/qq/official_webhook
/en/usage/platforms/readme
/en/usage/platforms/satori/llonebot
/en/usage/platforms/slack
/en/usage/platforms/telegram
/en/usage/platforms/webpage
/en/usage/platforms/wechat/weixin
/en/usage/platforms/wecom/wecom
/en/usage/platforms/wecom/wecombot
/en/usage/platforms/wecom/wecomcs
/en/usage/platforms/wxoa
/en/usage/sandbox/config
/en/usage/sandbox/readme
/en/usage/sandbox/runtime-relationships
/en/usage/skills/readme
/en/workshop/302ai-integration
/en/workshop/impl-platform-adapter
/en/workshop/mcp-details
/en/workshop/network-details
/en/workshop/newapi-integration
/en/workshop/ppio-integration
/en/workshop/production/proxy-and-ssl
/ja/api-reference/bots/创建机器人
/ja/api-reference/bots/删除机器人
/ja/api-reference/bots/更新机器人
/ja/api-reference/bots/获取单个机器人
/ja/api-reference/bots/获取所有机器人
/ja/api-reference/bots/获取机器人事件日志
/ja/api-reference/bots/通过机器人发送消息
/ja/api-reference/embedding-models/创建向量模型
/ja/api-reference/embedding-models/删除向量模型
/ja/api-reference/embedding-models/更新向量模型
/ja/api-reference/embedding-models/测试向量模型
/ja/api-reference/embedding-models/获取单个向量模型
/ja/api-reference/embedding-models/获取所有向量模型
/ja/api-reference/files/上传图片
/ja/api-reference/files/上传文档
/ja/api-reference/knowledge-bases/从知识库删除文件
/ja/api-reference/knowledge-bases/创建知识库
/ja/api-reference/knowledge-bases/删除知识库
/ja/api-reference/knowledge-bases/将文件存入知识库
/ja/api-reference/knowledge-bases/更新知识库
/ja/api-reference/knowledge-bases/检索知识库
/ja/api-reference/knowledge-bases/获取单个知识库
/ja/api-reference/knowledge-bases/获取所有知识库
/ja/api-reference/knowledge-bases/获取知识库文件列表
/ja/api-reference/knowledge-engines/获取可用知识引擎列表
/ja/api-reference/knowledge-engines/获取知识引擎创建配置模式
/ja/api-reference/knowledge-engines/获取知识引擎检索配置模式
/ja/api-reference/llm-models/创建-llm-模型
/ja/api-reference/llm-models/删除-llm-模型
/ja/api-reference/llm-models/更新-llm-模型
/ja/api-reference/llm-models/测试-llm-模型
/ja/api-reference/llm-models/获取单个-llm-模型
/ja/api-reference/llm-models/获取所有-llm-模型
/ja/api-reference/model-providers/创建供应商
/ja/api-reference/model-providers/删除供应商
/ja/api-reference/model-providers/更新供应商
/ja/api-reference/model-providers/获取单个供应商
/ja/api-reference/model-providers/获取所有供应商
/ja/api-reference/parsers/获取可用解析器列表
/ja/api-reference/pipelines/创建流水线
/ja/api-reference/pipelines/删除流水线
/ja/api-reference/pipelines/复制流水线
/ja/api-reference/pipelines/更新流水线
/ja/api-reference/pipelines/更新流水线扩展配置
/ja/api-reference/pipelines/获取单个流水线
/ja/api-reference/pipelines/获取所有流水线
/ja/api-reference/pipelines/获取流水线元数据
/ja/api-reference/pipelines/获取流水线扩展配置
/ja/api-reference/plugins/从-github-安装插件
/ja/api-reference/plugins/从-marketplace-安装插件
/ja/api-reference/plugins/删除插件
/ja/api-reference/plugins/升级插件
/ja/api-reference/plugins/更新插件配置
/ja/api-reference/plugins/获取已安装插件列表
/ja/api-reference/plugins/获取插件详情
/ja/api-reference/plugins/获取插件配置
/ja/api-reference/system/获取系统信息
/ja/api-reference/ツール/ツール詳細を取得
/ja/api-reference/ツール/利用可能なツール一覧を取得
/ja/articles
/ja/articles/connect-deepseek-to-wechat
/ja/articles/deploy-ai-bot-in-5-minutes
/ja/articles/dify-agent-discord-telegram-slack
/ja/articles/dify-langbot-rag-knowledge-base
/ja/articles/langbot-cloud-multi-user-workspaces
/ja/articles/langbot-plugin-system-deep-dive
/ja/articles/langbot-v4100-sandbox-skills
/ja/articles/langbot-v490-rag-plugin-architecture
/ja/articles/langflow-drag-and-drop-ai-bot
/ja/articles/langtars-remote-pc-control-dify-n8n
/ja/articles/n8n-multi-platform-ai-chatbot
/ja/articles/welcome
/ja/deploy/command
/ja/deploy/langbot/docker
/ja/deploy/langbot/kubernetes
/ja/deploy/langbot/manual
/ja/deploy/langbot/one-click/1panel
/ja/deploy/langbot/one-click/bt
/ja/deploy/langbot/package
/ja/deploy/settings
/ja/deploy/update
/ja/develop/agent-context
/ja/develop/comp-arch
/ja/develop/dev-config
/ja/develop/plugin-runtime
/ja/insight/community
/ja/insight/data-collection-policy
/ja/insight/features
/ja/insight/guide
/ja/insight/platform-features
/ja/insight/troubleshooting
/ja/plugin/compatibility
/ja/plugin/dev/apis/common
/ja/plugin/dev/apis/messages
/ja/plugin/dev/apis/pipeline-events
/ja/plugin/dev/apis/tech-details
/ja/plugin/dev/basic-info
/ja/plugin/dev/components/add
/ja/plugin/dev/components/command
/ja/plugin/dev/components/event-listener
/ja/plugin/dev/components/knowledge-engine
/ja/plugin/dev/components/knowledge-retriever
/ja/plugin/dev/components/page
/ja/plugin/dev/components/parser
/ja/plugin/dev/components/tool
/ja/plugin/dev/directory-structure
/ja/plugin/dev/migration
/ja/plugin/dev/publish/github
/ja/plugin/dev/publish/market
/ja/plugin/dev/style
/ja/plugin/dev/tutor
/ja/plugin/plugin-intro
/ja/tags/readme
/ja/tags/webhook
/ja/usage/knowledge/readme
/ja/usage/mcp/readme
/ja/usage/models/readme
/ja/usage/pipelines/coze
/ja/usage/pipelines/deerflow
/ja/usage/pipelines/dify
/ja/usage/pipelines/langflow
/ja/usage/pipelines/n8n
/ja/usage/pipelines/readme
/ja/usage/pipelines/weknora
/ja/usage/platforms/dingtalk
/ja/usage/platforms/discord
/ja/usage/platforms/http-bot
/ja/usage/platforms/kook
/ja/usage/platforms/lark
/ja/usage/platforms/line
/ja/usage/platforms/qq/aiocqhttp/lagrange
/ja/usage/platforms/qq/aiocqhttp/llonebot
/ja/usage/platforms/qq/aiocqhttp/napcat
/ja/usage/platforms/qq/official_webhook
/ja/usage/platforms/readme
/ja/usage/platforms/satori/llonebot
/ja/usage/platforms/slack
/ja/usage/platforms/telegram
/ja/usage/platforms/webpage
/ja/usage/platforms/wechat/weixin
/ja/usage/platforms/wecom/wecom
/ja/usage/platforms/wecom/wecombot
/ja/usage/platforms/wecom/wecomcs
/ja/usage/platforms/wxoa
/ja/usage/sandbox/config
/ja/usage/sandbox/readme
/ja/usage/sandbox/runtime-relationships
/ja/usage/skills/readme
/ja/workshop/302ai-integration
/ja/workshop/impl-platform-adapter
/ja/workshop/mcp-details
/ja/workshop/network-details
/ja/workshop/newapi-integration
/ja/workshop/ppio-integration
/ja/workshop/production/proxy-and-ssl
/scripts/README-blog-articles
/zh/api-reference/向量模型/创建向量模型
/zh/api-reference/向量模型/删除向量模型
/zh/api-reference/向量模型/更新向量模型
/zh/api-reference/向量模型/测试向量模型
/zh/api-reference/向量模型/获取单个向量模型
/zh/api-reference/向量模型/获取所有向量模型
/zh/api-reference/大语言模型/创建-llm-模型
/zh/api-reference/大语言模型/删除-llm-模型
/zh/api-reference/大语言模型/更新-llm-模型
/zh/api-reference/大语言模型/测试-llm-模型
/zh/api-reference/大语言模型/获取单个-llm-模型
/zh/api-reference/大语言模型/获取所有-llm-模型
/zh/api-reference/工具/获取工具详情
/zh/api-reference/工具/获取所有可用工具
/zh/api-reference/插件/从-github-安装插件
/zh/api-reference/插件/从-marketplace-安装插件
/zh/api-reference/插件/删除插件
/zh/api-reference/插件/升级插件
/zh/api-reference/插件/更新插件配置
/zh/api-reference/插件/获取已安装插件列表
/zh/api-reference/插件/获取插件详情
/zh/api-reference/插件/获取插件配置
/zh/api-reference/文件/上传图片
/zh/api-reference/文件/上传文档
/zh/api-reference/机器人/创建机器人
/zh/api-reference/机器人/删除机器人
/zh/api-reference/机器人/更新机器人
/zh/api-reference/机器人/获取单个机器人
/zh/api-reference/机器人/获取所有机器人
/zh/api-reference/机器人/获取机器人事件日志
/zh/api-reference/机器人/通过机器人发送消息
/zh/api-reference/模型供应商/创建供应商
/zh/api-reference/模型供应商/删除供应商
/zh/api-reference/模型供应商/更新供应商
/zh/api-reference/模型供应商/获取单个供应商
/zh/api-reference/模型供应商/获取所有供应商
/zh/api-reference/流水线/创建流水线
/zh/api-reference/流水线/删除流水线
/zh/api-reference/流水线/复制流水线
/zh/api-reference/流水线/更新流水线
/zh/api-reference/流水线/更新流水线扩展配置
/zh/api-reference/流水线/获取单个流水线
/zh/api-reference/流水线/获取所有流水线
/zh/api-reference/流水线/获取流水线元数据
/zh/api-reference/流水线/获取流水线扩展配置
/zh/api-reference/知识库/从知识库删除文件
/zh/api-reference/知识库/创建知识库
/zh/api-reference/知识库/删除知识库
/zh/api-reference/知识库/将文件存入知识库
/zh/api-reference/知识库/更新知识库
/zh/api-reference/知识库/检索知识库
/zh/api-reference/知识库/获取单个知识库
/zh/api-reference/知识库/获取所有知识库
/zh/api-reference/知识库/获取知识库文件列表
/zh/api-reference/知识引擎/获取可用知识引擎列表
/zh/api-reference/知识引擎/获取知识引擎创建配置模式
/zh/api-reference/知识引擎/获取知识引擎检索配置模式
/zh/api-reference/系统/获取系统信息
/zh/api-reference/解析器/获取可用解析器列表
/zh/articles
/zh/articles/connect-deepseek-to-wechat
/zh/articles/deploy-ai-bot-in-5-minutes
/zh/articles/dify-agent-discord-telegram-slack
/zh/articles/dify-langbot-rag-knowledge-base
/zh/articles/langbot-cloud-multi-user-workspaces
/zh/articles/langbot-plugin-system-deep-dive
/zh/articles/langbot-v4100-sandbox-skills
/zh/articles/langbot-v490-rag-plugin-architecture
/zh/articles/langflow-drag-and-drop-ai-bot
/zh/articles/langtars-remote-pc-control-dify-n8n
/zh/articles/n8n-multi-platform-ai-chatbot
/zh/articles/welcome
/zh/deploy/command
/zh/deploy/langbot/docker
/zh/deploy/langbot/kubernetes
/zh/deploy/langbot/manual
/zh/deploy/langbot/one-click/1panel
/zh/deploy/langbot/one-click/bt
/zh/deploy/langbot/package
/zh/deploy/settings
/zh/deploy/update
/zh/develop/adapter/discord/README
/zh/develop/adapter/discord/api_reference
/zh/develop/adapter/discord/design
/zh/develop/adapter/discord/quick_start
/zh/develop/adapter/discord/troubleshooting
/zh/develop/agent-context
/zh/develop/comp-arch
/zh/develop/dev-config
/zh/develop/plugin-runtime
/zh/insight/community
/zh/insight/data-collection-policy
/zh/insight/features
/zh/insight/guide
/zh/insight/platform-features
/zh/insight/troubleshooting
/zh/plugin/compatibility
/zh/plugin/dev/apis/common
/zh/plugin/dev/apis/messages
/zh/plugin/dev/apis/pipeline-events
/zh/plugin/dev/apis/tech-details
/zh/plugin/dev/basic-info
/zh/plugin/dev/components/add
/zh/plugin/dev/components/command
/zh/plugin/dev/components/event-listener
/zh/plugin/dev/components/knowledge-engine
/zh/plugin/dev/components/knowledge-retriever
/zh/plugin/dev/components/page
/zh/plugin/dev/components/parser
/zh/plugin/dev/components/tool
/zh/plugin/dev/directory-structure
/zh/plugin/dev/migration
/zh/plugin/dev/publish/github
/zh/plugin/dev/publish/market
/zh/plugin/dev/style
/zh/plugin/dev/tutor
/zh/plugin/plugin-intro
/zh/tags/readme
/zh/tags/webhook
/zh/tob
/zh/tob/contact
/zh/tob/platform
/zh/tob/provider
/zh/usage/knowledge/readme
/zh/usage/mcp/readme
/zh/usage/models/readme
/zh/usage/pipelines/coze
/zh/usage/pipelines/deerflow
/zh/usage/pipelines/dify
/zh/usage/pipelines/langflow
/zh/usage/pipelines/n8n
/zh/usage/pipelines/readme
/zh/usage/pipelines/tbox
/zh/usage/pipelines/weknora
/zh/usage/platforms/dingtalk
/zh/usage/platforms/discord
/zh/usage/platforms/http-bot
/zh/usage/platforms/kook
/zh/usage/platforms/lark
/zh/usage/platforms/line
/zh/usage/platforms/qq/aiocqhttp/lagrange
/zh/usage/platforms/qq/aiocqhttp/llonebot
/zh/usage/platforms/qq/aiocqhttp/napcat
/zh/usage/platforms/qq/official
/zh/usage/platforms/qq/official_webhook
/zh/usage/platforms/readme
/zh/usage/platforms/satori/llonebot
/zh/usage/platforms/slack
/zh/usage/platforms/telegram
/zh/usage/platforms/webpage
/zh/usage/platforms/wechat/wechatpad
/zh/usage/platforms/wechat/weixin
/zh/usage/platforms/wecom/wecom
/zh/usage/platforms/wecom/wecombot
/zh/usage/platforms/wecom/wecomcs
/zh/usage/platforms/wxoa
/zh/usage/sandbox/config
/zh/usage/sandbox/readme
/zh/usage/sandbox/runtime-relationships
/zh/usage/skills/readme
/zh/workshop/302ai-integration
/zh/workshop/impl-platform-adapter
/zh/workshop/mcp-details
/zh/workshop/network-details
/zh/workshop/newapi-integration
/zh/workshop/ppio-integration
/zh/workshop/production/proxy-and-ssl
/zh/workshop/shengsuanyun-integration
/zh/workshop/tokenpony-integration
+9 -6
View File
@@ -144,14 +144,15 @@ 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 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);
const lines = renderCloudflareRedirects(docs).trim().split("\n");
assert.equal(lines.length, docs.redirects.length + 3);
assert.equal(lines[0], "/ /en/insight/guide 302");
assert.ok(lines.includes("/README_EN /en/insight/guide 308"));
assert.ok(lines.includes("/zh/develop/adapter/discord /zh/develop/adapter/discord/README 308"));
assert.ok(lines.includes("/scripts/README-blog-articles /en/articles 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"));
assert.ok(lines.includes("/en/api-reference/system/获取系统信息 /en/api-reference/system/get-system-information 308"));
assert.ok(lines.includes("/en/api-reference/model-providers/创建供应商 /en/api-reference/model-providers/create-a-model-provider 308"));
});
test("Fumapress config wires static localized docs, OpenAPI, and Mintlify support", async () => {
@@ -165,6 +166,8 @@ test("Fumapress config wires static localized docs, OpenAPI, and Mintlify suppor
assert.match(config, new RegExp(`input:[\\s\\S]*openapi/${filename}`));
assert.match(config, new RegExp(`staticSource\\(\\{\\s*baseDir:\\s*["']${locale}/api-reference["']`));
}
assert.match(config, /groupBy:\s*["']tag["']/);
assert.match(config, /legacyOpenApiSlug/);
assert.match(config, /mintlifyPlugin\(/);
assert.match(config, /openapiPlugin\(\{\s*server:\s*\w+OpenAPI\s*\}\)/);
assert.match(config, /fumadocsMdx\(/);
@@ -179,7 +182,7 @@ test("prebuild is deterministic and preserves local assets and SEO files", async
assert.equal(first.documents, 302);
assert.equal(first.fallbackDefaults, 0);
assert.equal(first.localeOnlyDocuments, 14);
assert.equal(first.redirects, 102);
assert.equal(first.redirects, 104);
assert.deepEqual(first.locales, ["en", "zh", "ja"]);
await readFile(path.join(temp, "content/docs/insight/guide.mdx"), "utf8");
await readFile(path.join(temp, "content/docs/insight/guide.zh.mdx"), "utf8");
+38 -17
View File
@@ -67,24 +67,36 @@ test("representative localized guides and canonical host are emitted", async ()
test("all locales emit the OpenAPI surface with endpoint semantics", async () => {
const representatives = {
en: ["Get system information", "GET", "/api/v1/system/info"],
zh: ["获取系统信息", "GET", "/api/v1/system/info"],
ja: ["获取系统信息", "GET", "/api/v1/system/info"],
en: ["system/get-system-information", "Get system information", "GET", "/api/v1/system/info"],
zh: ["系统/获取系统信息", "获取系统信息", "GET", "/api/v1/system/info"],
ja: ["system/获取系统信息", "获取系统信息", "GET", "/api/v1/system/info"],
};
const sitemap = await readFile(path.join(publicRoot, "sitemap.xml"), "utf8");
for (const [locale, semantics] of Object.entries(representatives)) {
for (const [locale, [representativePath, ...semantics]] of Object.entries(representatives)) {
const apiRoot = path.join(publicRoot, locale, "api-reference");
const routes = await collectHtmlRoutes(apiRoot);
assert.ok(routes.length >= 50, `${locale} emitted only ${routes.length} API routes`);
const representative = path.join(apiRoot, "api/v1/system/info/get/index.html");
const encodedRepresentativePath = representativePath.split("/").map((segment) => encodeURIComponent(segment)).join("/");
const representative = path.join(apiRoot, encodedRepresentativePath, "index.html");
const html = await readFile(representative, "utf8");
for (const semantic of semantics) assert.ok(html.includes(semantic), `${locale} page is missing ${semantic}`);
assert.ok(sitemap.includes(`https://docs.langbot.dev/${locale}/api-reference/api/v1/system/info/get`));
assert.ok(sitemap.includes(`https://docs.langbot.dev/${locale}/api-reference/${encodedRepresentativePath}`));
}
});
test("canonical sitemap routes match the legacy Mintlify directory structure", async () => {
const legacy = new Set((await readFile(path.join(root, "tests/fixtures/legacy-sitemap-routes.txt"), "utf8"))
.trim().split("\n").map((route) => route.replace(/\/$/, "")));
// Mintlify accidentally indexed this repository maintenance note; preserve
// its URL as a redirect without treating it as public documentation.
legacy.delete("/scripts/README-blog-articles");
const sitemap = await readFile(path.join(publicRoot, "sitemap.xml"), "utf8");
const actual = new Set([...sitemap.matchAll(/<loc>https:\/\/docs\.langbot\.dev(\/[^<]+)<\/loc>/g)]
.map((match) => decodeURIComponent(match[1]).replace(/\/$/, "")));
assert.deepEqual([...actual].sort(), [...legacy].sort());
});
const locales = ["en", "zh", "ja"];
const hreflangByLocale = { en: "en", zh: "zh-CN", ja: "ja" };
@@ -120,11 +132,11 @@ function extractHeadLinks(html, rel) {
}));
}
test("rendered navbar uses each locale's docs.json labels and hrefs", async () => {
test("rendered navbar and documentation tree use each locale's docs.json labels", async () => {
const expected = {
en: ["Home", "https://langbot.app/en", "Roadmap", "https://langbot.app/en/roadmap"],
zh: ["首页", "https://langbot.app/zh", "路线图", "https://langbot.app/zh/roadmap"],
ja: ["ホーム", "https://langbot.app/ja", "ロードマップ", "https://langbot.app/ja/roadmap"],
en: ["Home", "https://langbot.app/en", "Roadmap", "https://langbot.app/en/roadmap", "Guides", "Quick Start", "Installation", "Developers", "Articles", "API Reference"],
zh: ["首页", "https://langbot.app/zh", "路线图", "https://langbot.app/zh/roadmap", "指南", "快速开始", "安装部署", "开发者", "文章", "API 参考"],
ja: ["ホーム", "https://langbot.app/ja", "ロードマップ", "https://langbot.app/ja/roadmap", "ガイド", "クイックスタート", "インストール", "開発者", "記事", "API リファレンス"],
};
for (const locale of locales) {
const html = await readFile(path.join(publicRoot, locale, "insight/guide/index.html"), "utf8");
@@ -179,18 +191,27 @@ test("every page emits a canonical and reciprocal available hreflangs", async ()
const [, locale, ...suffixParts] = route.split("/");
const suffix = suffixParts.join("/");
const expected = {};
for (const candidate of locales) {
const candidateRoute = `/${candidate}/${suffix}`.replace(/\/$/, "");
if (routes.has(candidateRoute)) expected[hreflangByLocale[candidate]] = `https://docs.langbot.dev${candidateRoute}`;
}
expected["x-default"] = expected.en ?? expected["zh-CN"] ?? expected.ja;
const actual = Object.fromEntries(
extractHeadLinks(html, "alternate")
.filter((item) => item.hreflang)
.map((item) => [item.hreflang, item.href]),
);
assert.deepEqual(actual, expected, route);
if (suffix.startsWith("api-reference/")) {
assert.deepEqual(Object.keys(actual).sort(), ["en", "ja", "x-default", "zh-CN"]);
assert.equal(actual["x-default"], actual.en);
for (const href of Object.values(actual)) {
const target = new URL(href).pathname.replace(/\/$/, "");
assert.ok(routes.has(target), `${route} links to missing alternate ${target}`);
}
} else {
const expected = {};
for (const candidate of locales) {
const candidateRoute = `/${candidate}/${suffix}`.replace(/\/$/, "");
if (routes.has(candidateRoute)) expected[hreflangByLocale[candidate]] = `https://docs.langbot.dev${candidateRoute}`;
}
expected["x-default"] = expected.en ?? expected["zh-CN"] ?? expected.ja;
assert.deepEqual(actual, expected, route);
}
}
});