docs/solutions/developer-experience/2026-05-24-fumadocs-page-tree-search-needs-locale-safe-metadata.md
After the docs source cutover, apps/www still used docsConfig as the runtime source for sidebar navigation, pager links, and command-menu docs search. Moving those surfaces to Fumadocs exposed a separate i18n search problem: Plate's cn locale is not a valid Orama tokenizer language.
DocsNav, pager, mobile docs nav, and command-menu docs entries were still driven by docsConfig.content/docs/meta.json existed.docs-page-tree.ts still imported docsConfig/docsMap after the pageTree switch, keeping the old TS nav graph in runtime code for descriptions, labels, keywords, and CN labels.docsConfig through docs-utils, keeping the old TS nav graph in the browser bundle./docs/components to /cn/docs/components, but those category pages did not have explicit CN app routes./cn/docs/[[...slug]] copied the English catch-all route almost line-for-line, so registry fallback pages, related docs, metadata, pager inputs, and highlighted-source loading could drift by locale.table hit /api/search?query=table, then the dev server logged:Error: Language "cn" is not supported.
source.getPageTree() into components. Without metadata, the tree falls back to filesystem order and loses Plate's product navigation shape.createFromSource(source) with no options. With Fumadocs i18n enabled, it builds per-locale Orama indexes and passes locale names through as tokenizer languages.docsConfig as gone immediately. Registry-derived and app-only docs links still need a migration overlay until they become Fumadocs metadata or registry source./docs/* link without route parity for app-only pages. Fumadocs can resolve MDX-backed CN docs and English fallbacks, but it cannot render category pages that only exist as app routes.Generate a root metadata file from the existing navigation data, then read Fumadocs pageTree at runtime:
export function getSidebarNavFromPageTree(locale: string = 'en') {
const tree = source.getPageTree(locale);
// convert separators and page nodes into SidebarNavItem sections
}
Use the same pageTree for pager neighbours:
const neighbours = findNeighbour(source.getPageTree(locale), href);
Replace client-only docs search with the Fumadocs search API:
import { createFromSource } from 'fumadocs-core/search/server';
import { source } from '@/lib/source';
export const { GET } = createFromSource(source, {
localeMap: {
cn: 'english',
},
});
Use docsConfig only as migration data for labels, Chinese titles, and the first metadata generator. Once content/docs/meta.json carries the page list and _plate overlays, delete the generator and the TS nav files instead of keeping both systems alive.
Move runtime-only overlay data into the committed metadata file itself. The generator can still consume docsConfig temporarily, but runtime code should read content/docs/meta.json:
{
"root": true,
"pages": ["---Get Started---", "[Introduction](/docs)"],
"_plate": {
"docSections": [{ "items": [] }],
"categoryGroups": {
"component": []
},
"sections": {
"Get Started": "开始"
},
"items": {
"/docs/plugin-shortcuts": {
"label": "Updated",
"titleCn": "插件快捷键"
}
}
}
}
Then centralize overlay lookup beside the Fumadocs pageTree adapter:
import docsMeta from '../../../../content/docs/meta.json';
const docsOverlay = (docsMeta as { _plate?: DocsMetaOverlay })._plate ?? {};
export function getDocsNavMeta(href: string) {
return docsOverlay.items?.[normalizeDocsHref(href)];
}
Keep client-safe metadata reads separate from the server-only pageTree adapter. Client components such as category grids and breadcrumb switchers should import a JSON-only helper, not the Fumadocs server module:
export function getDocsCategoryGroups(category: DocsCategory | string) {
return docsOverlay.categoryGroups?.[category] ?? [];
}
The final cleanup should make committed metadata the only docs navigation source:
content/docs/meta.json
pages
_plate.sections
_plate.items
_plate.docSections
_plate.categoryGroups
At that point, remove the old sync bridge:
apps/www/scripts/sync-docs-meta.mts
apps/www/src/config/docs*.ts
apps/www/src/config/nav-to-object.ts
apps/www/src/config/registry-to-nav.ts
This makes metadata edits explicit and matches upstream shadcn's hand-authored content/docs/**/meta.json model.
For app-only docs surfaces that the CN nav can link to, add explicit /cn/docs/... routes that reuse the retained page UI. That covers category roots and special examples that are not physical MDX files.
For catch-all docs pages, keep locale-specific Next route files thin and route everything through one shared server helper:
export function generateStaticParams() {
return generateDocStaticParams('cn');
}
export function generateMetadata(props: DocPageProps) {
return generateDocMetadata(props, 'cn');
}
export default function CNDocPage(props: DocPageProps) {
return renderDocPage(props, 'cn');
}
The shared helper should own Fumadocs source.getPage(...), registry fallback pages, ComponentInstallation, ComponentPreview, related-doc inference, metadata generation, and pager lookup. Localize internal related-doc routes in that helper with the same hrefWithLocale behavior used by navigation, so CN registry fallback pages do not link users back to English docs.
Fumadocs meta.json is the pageTree ordering contract. Root pages entries can include separators and links, which lets Plate represent app-only and registry-derived docs routes without pretending every route is a physical MDX file. Keep that metadata under content/docs/ with the MDX files, matching the upstream shadcn/Fumadocs content root.
Fumadocs' built-in meta schema only exposes fields such as pages, title, and description to the pageTree. Keeping Plate-specific overlay fields under _plate lets the app read them directly from the committed metadata file while Fumadocs continues to ignore unknown data during source generation.
Fumadocs search delegates tokenization to Orama. Orama supports english, not Plate's locale key cn; mapping cn to a supported tokenizer keeps the i18n search index buildable while preserving /cn/docs/* routing.
content/docs/meta.json before switching visible navigation to source.pageTree.docsConfig usage centralized in metadata generation/parity scripts, not runtime page rendering.content/docs/meta.json carries pages, labels, CN titles, sections, category tabs, and category groups._plate.sections, _plate.items, _plate.docSections, and _plate.categoryGroups values in docs parity so regenerated metadata cannot silently drop labels, CN titles, category tabs, or grid groups.docsConfig/sync-docs-meta before shipping the slice./docs and /cn/docs/* after changing search or pageTree code.