Back to Plate

Fumadocs pageTree search needs locale-safe metadata

docs/solutions/developer-experience/2026-05-24-fumadocs-page-tree-search-needs-locale-safe-metadata.md

53.0.88.5 KB
Original Source

Fumadocs pageTree search needs locale-safe metadata

Problem

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.

Symptoms

  • DocsNav, pager, mobile docs nav, and command-menu docs entries were still driven by docsConfig.
  • Fumadocs pageTree had no useful ordering until 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.
  • Category grid pages and the breadcrumb switcher still imported docsConfig through docs-utils, keeping the old TS nav graph in the browser bundle.
  • CN navigation localized app-only category links such as /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.
  • Browser search for table hit /api/search?query=table, then the dev server logged:
text
Error: Language "cn" is not supported.

What Didn't Work

  • Only wiring source.getPageTree() into components. Without metadata, the tree falls back to filesystem order and loses Plate's product navigation shape.
  • Calling createFromSource(source) with no options. With Fumadocs i18n enabled, it builds per-locale Orama indexes and passes locale names through as tokenizer languages.
  • Treating docsConfig as gone immediately. Registry-derived and app-only docs links still need a migration overlay until they become Fumadocs metadata or registry source.
  • Localizing every /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.

Solution

Generate a root metadata file from the existing navigation data, then read Fumadocs pageTree at runtime:

ts
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:

ts
const neighbours = findNeighbour(source.getPageTree(locale), href);

Replace client-only docs search with the Fumadocs search API:

ts
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:

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:

ts
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:

ts
export function getDocsCategoryGroups(category: DocsCategory | string) {
  return docsOverlay.categoryGroups?.[category] ?? [];
}

The final cleanup should make committed metadata the only docs navigation source:

text
content/docs/meta.json
  pages
  _plate.sections
  _plate.items
  _plate.docSections
  _plate.categoryGroups

At that point, remove the old sync bridge:

text
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:

ts
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.

Why This Works

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.

Prevention

  • Add content/docs/meta.json before switching visible navigation to source.pageTree.
  • For Fumadocs i18n search, explicitly map custom locale keys to tokenizer languages supported by Orama.
  • Keep temporary docsConfig usage centralized in metadata generation/parity scripts, not runtime page rendering.
  • Delete that temporary generator once parity proves content/docs/meta.json carries pages, labels, CN titles, sections, category tabs, and category groups.
  • Assert representative _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.
  • After deleting the generator, keep the parity check pointed at committed metadata and add an active-source search for docsConfig/sync-docs-meta before shipping the slice.
  • When localizing app-only docs links, add matching CN app routes or a parity check that proves they already exist.
  • Do not keep separate English and CN catch-all implementations. Keep separate route files only for Next's route exports, then call a shared locale-aware renderer.
  • When refactoring localized docs routes, smoke both MDX-backed pages and registry fallback pages in English and CN.
  • For localized sidebars without physical localized MDX files, reuse the default pageTree and localize labels/hrefs at render time. Keep accordion open state derived from the current route or filtered result set, not from DOM queries or delayed scroll timers.
  • Browser-test both /docs and /cn/docs/* after changing search or pageTree code.