Neoverse-Docs
About the Project

Search, Navigation, and Community

Static search, result enhancement, chapter filtering, cross-document return, source endpoints, i18n, and Giscus

Primary author:
AI Summary of This Chapter

The project statically generates the search index, page tree, and source endpoints, then adds Chapter scope filtering, a search result enhancement pipeline, a search spotlight, and cross-document reading return in the browser. Chinese search uses mixed Chinese-English tokenization and adds pinyin lookup only for titles and section headings. Giscus theme switches eliminate flashing through useLayoutEffect and a fixed 400ms fade. Route transitions are covered separately in a dedicated topic.

1. Static search architecture

src/content/search/index.ts uses Fumadocs createFromSource() to build search data from the content source; src/app/api/search/route.ts only sets dynamic = 'force-static' and exports the static GET. Production builds output this Route Handler as a static endpoint, so no request-time Markdown scanning is needed.

The Fumadocs page-level index ID uses stable docs:<id>:<locale>, while navigation still uses URLs. Full-text index records retain the page URL, title, description, and structured content; the first-level slug remains the raw tag, so existing Chapter filtering directly reuses the content path without a second chapter mapping.

Search Schema v2 and metadata join

src/content/search/schema.ts defines the application-level SearchDocument: every page, description, heading, or body record can express contentId, locale, title, description, optional heading ID / title, body, plus contentType, Topic, Track, and Difficulty. It uses contentType rather than type to avoid colliding with Fumadocs' page / heading / text record type.

Body text continues to come only from Fumadocs structured content. While building the index, src/content/search/index.ts joins taxonomy from the Manifest-derived Search Metadata Projection by stable contentId + locale; a missing match fails the build rather than silently producing a record without metadata. Body text never enters Content IR or the metadata sidecar.

The existing static search engine remains unchanged. To work with its fixed schema, the raw Chapter tag is supplemented with namespaced tags such as track:<id>, topic:<id>, content-type:<id>, and difficulty:<id>. The static /api/search-metadata sidecar emits this metadata by stable page-level search ID for a future Search UI or result enhancer; the current Search Dialog neither fetches it nor adds filter controls.

Default English tokenization can't reliably handle continuous Chinese, while simply splitting by character would damage English words and phrases. src/content/search/tokenizer.ts combines both behaviors:

  • Chinese fragments use the dictionary tokenization of @orama/tokenizers/mandarin;
  • English fragments are uniformly lowercased, keeping English queries case-insensitive;
  • Chinese titles and section headings additionally get pinyin forms;
  • The content isn't fully expanded to pinyin, avoiding index bloat and irrelevant matches;
  • English locales continue to use the English language configuration.

Namespace prefixes

The server index and client queries use different namespace prefixes to ensure pinyin is only searched when needed:

ConstantPrefixPurpose
PINYIN_INDEX_PREFIX\uE000The server only marks titles and section headings, generating pinyin aliases
PINYIN_QUERY_PREFIX\u0000pinyin:The browser only searches namespaced pinyin aliases during queries

Pinyin generation

getPinyinAliases() generates, for Chinese segments:

  • Full pinyin forms;
  • Initial-letter abbreviations;
  • Bounded adjacent segments (MAX_PINYIN_SEGMENT_SPAN = 8) to avoid overly long segments producing irrelevant matches.

addKeyboardUmlautVariant() handles the v → ü variant, supporting pinyin diacritics typed from the keyboard.

Before building the index, the server collects the titles that need pinyin expansion; the client initializes the Chinese database with the same mixed tokenization logic, avoiding inconsistency between the "indexing approach" and the "querying approach".

Chinese search sets strict threshold and tolerance values, reducing short words and single characters being fuzzy-matched to many irrelevant pages. English search still relies on the English language configuration for word forms and normalization.

3. Search result enhancement

withEnhancedSearch() in src/features/search/client.ts wraps the raw search client and processes results through the following pipeline:

Text
Raw search results
  → cleanSearchResultContent()       Strips pinyin index prefixes
  → rankSearchResultGroups()         Groups by page, sorts within groups by type priority
  → preferSearchResultAnchors()      The first page result of each group inherits the most relevant subsection URL
  → mergePinyinSearchResults()       Deduplicates and merges pinyin results
  → addSearchSpotlightParams()       Adds spotlight parameters to every result

Group sorting

rankSearchResultGroups():

  • Groups by page, then sorts within each group by type priority (page > heading > text);
  • Sorts groups by whether they contain <mark> highlights, with highlighted groups first.

Anchor-first

preferSearchResultAnchors(): the first page result of each group inherits the most relevant subsection URL, letting search results land directly on a specific section.

Pinyin merging

mergePinyinSearchResults():

  • Deduplicates and merges pinyin results;
  • Skips text types and already-seen ids;
  • Caps at 60 entries to avoid result bloat.

Spotlight parameters

addSearchSpotlightParams() adds a _searchSpotlight parameter to every result, carrying the most relevant text fragment for spotlight positioning after navigation.

4. Search dialog and scope

src/features/search/components/search-dialog.tsx uses Fumadocs useDocsSearch and the static client:

  • The Chinese locale uses createMixedTokenizer(), and others use english;
  • Wrapped with withEnhancedSearch(searchClient, locale === 'zh'): pinyin fallback, grouped result sorting, chapter anchor inheritance;
  • Chapter filter Popover: the lightweight search-dialog__scope-trigger keeps it at a secondary level.

Scope options come from the current locale's page tree:

  • "All content" passes no tag;
  • When a Chapter is selected, that root node's slug is used as tag;
  • Scopes that are no longer valid are reset when the locale changes;
  • Result and scope copy is read from the project dictionaries.

While the search dialog is closed, the component listens for Fumadocs' Ctrl + K / Cmd + K shortcuts in the capture phase. If the reader has kept a full selection inside the #nd-page content container, the selection is normalized into a single-line, space-separated query before the dialog opens, taking the first 200 characters (MAX_SELECTED_SEARCH_LENGTH); selections triggered by clicking the search entrance, selections extending outside the content, and selections inside editable controls all keep the original query behavior.

The scope menu is only a query constraint; it doesn't maintain separate content state. After a new Chapter is registered in the page tree, it naturally enters the search scopes.

Chapter remains the author's linear content arrangement. It is used only for the compatible search scope; it cannot derive a Topic and is not the classification source for Learn, Explore, or Reference. Query data for Topic, Track, Content Type, and Difficulty comes only from the content/search Projection / Schema interface, never from Search Feature frontmatter or taxonomy parsing.

5. Search spotlight

src/features/search/components/search-spotlight.tsx implements a short-lived global spotlight after navigating from a search result:

  • Listens to click (link / button[aria-selected]), keydown Enter, hashchange, and popstate;
  • findTextRange(): walks text nodes with a TreeWalker to find the target text after the anchor;
  • getRangeRect(): merges multi-line rects;
  • clampSpotlightRect(): 12px padding, clamped within the viewport;
  • centerSpotlightRect(): scrolls to center the target;
  • ResizeObserver + MutationObserver track layout changes;
  • Closes after interaction (pointerdown / wheel / touchstart / Escape);
  • SPOTLIGHT_DURATION_MS = 3200 (1800 with reduced motion);
  • removeSpotlightParam(): removes the parameter from the URL when done.

src/features/search/spotlight.ts provides spotlight parameter handling:

  • SEARCH_SPOTLIGHT_PARAM = '_searchSpotlight';
  • getSpotlightScrollDelta(): aligns the target rect's center with the viewport center;
  • cleanSpotlightText(): strips HTML tags and markdown decorations, truncating to 120 characters;
  • getResultGroupTarget(): prefers the <mark> highlighted text, and page types borrow the target from a subsection.

6. Page tree and navigation

content/docs/{locale}/meta.json and subdirectory meta.json files together form the Fumadocs page tree. The docs layout uses the tree for the current locale, so Chinese and English can have different coverage without needing empty pages to maintain perfect symmetry.

The Fumadocs page tree provides:

  • Left-side Chapter, Stage, and page navigation;
  • Previous / next relationships for the current page;
  • The current page's TOC and scroll position;
  • The source of homepage chapter entrances and search scopes.

Section headings are recorded in the pages array in ---title--- form. Page order comes from the explicit configuration rather than relying on filesystem sorting.

7. Cross-document reading return

Plain browser history can only go back to the source page; it can't reliably restore the context the reader had when clicking a link. If a long document has images, code, or deferred-layout content near the top, restoring the absolute scrollY can also drift.

DocsReadingReturn records, when a content link is clicked:

  • The source and target pathnames;
  • The source page title;
  • The link's element path relative to data-docs-body;
  • The viewport position when leaving the link;
  • An absolute scroll position as fallback.

After reaching the target document, the page shows the "return to reading position" action. When returning to the source, the component first finds the same link by element path, then restores it to the screen height at which the reader left; only when the element path fails does it use the absolute position.

This metadata is kept in sessionStorage, and the content is never saved or cloned. External links, new-window links, download links, same-page hashes, and previous / next navigation keep their native behavior.

Document refresh restore

src/features/reading/restore.ts handles scroll restoration after a page refresh:

  • DOCS_REFRESH_RESTORE_BOOTSTRAP is an inline script that runs before hydration;
  • Runs only when navigation.type === 'reload' and a valid anchor exists in sessionStorage;
  • Sets the data-nd-reading-restore attribute so the client restores the scroll position.

8. Documentation source and GitHub actions

Documentation pages derive the GitHub file URL from the content source's fullPath, and also generate docs-source URLs by locale and slug.

src/app/[lang]/docs-source/[...slug]/route.ts enumerates all pages at build time and returns the raw Markdown / MDX content. Output paths use the .md suffix to avoid conflicts between page directories and identically named files in static hosting.

DocsPageActions places "View source" and "Open in GitHub" on the same line as the author info. When readers find an issue, they can directly locate the source file without manually guessing the repo directory from the page URL.

9. i18n boundaries

src/lib/i18n.ts is the single source for supported languages and the default language. It drives:

  • The locale buckets of the Fumadocs content loader;
  • The language parameters of generateStaticParams();
  • The language context of the Fumadocs UI;
  • The language of custom error pages, loading pages, and comments.

UI translations are split into two layers:

  • src/adapters/fumadocs/layout.tsx extends Fumadocs' built-in UI translations (including keys added in 16.13.x such as Ask AI, Close Sidebar, Layout Tab);
  • src/dictionaries/ holds the project's custom copy, with the key structure constrained by a TypeScript interface.

The UI supporting Chinese and English doesn't mean every article is translated. Whether a page exists is decided by the actual files in content/docs/{locale}/; English pages don't use empty-shell placeholders to simulate full coverage.

10. Community modules

The guestbook and doc comments use Giscus with GitHub Discussions. DocsCommunity places the comment area outside the content card, and Guestbook chooses Giscus language and theme based on the locale and theme.

Giscus configuration

  • slugKey serves as the discussion identifier, and Chinese and English pages share the same discussion thread;
  • mapping="specific", inputPosition="top", reactionsEnabled="1";
  • GISCUS_LANG_MAP: zh → zh-CN, en → en;
  • Theme URL: jsDelivr CDN in production, site origin joined with relative paths in development.

Eliminating theme-switch flashing

Giscus runs in a cross-origin iframe. When the theme switches, the iframe reloads the theme CSS, causing a brief unstyled flash in between. The project eliminates it in the following ways:

  • useLayoutEffect (instead of useEffect) synchronously sets the data-switching attribute after React commit and before paint, ensuring opacity:0 takes effect before the browser draws;
  • A prevThemeUrl ref tracks the previous theme, triggering only when switching between two valid URLs;
  • A fixed 400ms fade duration, without waiting for the resizeHeight message (which is only sent back when the height changes, and theme switches usually don't change the height);
  • CSS coordination: transition: none hides immediately, opacity 240ms fades back in.

useEffect runs after the browser paint, leaving a paint window where unstyled content flashes; useLayoutEffect moves the opacity:0 application ahead of paint, eliminating this window.

Giscus remains an external enhancement: when the network is unavailable or the user has no GitHub account, the document content, search, and navigation are unaffected.

11. Where interactive state lives

StateStorage locationLifecycle
Theme preferenceBrowser local storageAcross pages and revisits
Motion preferenceBrowser local storageAcross pages and revisits
Sidebar collapseBrowser local storageAcross pages and revisits
Task completion stateLocal storage keyed by pathnameAcross pages and revisits
Tabs preferenceBrowser local storageWhen persist is enabled
Reading return pointsessionStorageCurrent tab session
Transition DOM cloneMemorySingle transition

None of this state needs an account or a database, and it doesn't sync across devices. Static-first doesn't mean rejecting state; it keeps light, private, collaboration-free data in the browser.

12. Key files

FileResponsibility
src/app/api/search/route.tsStatic search index
src/app/api/search-metadata/route.tsStatic Search metadata sidecar
src/content/search/index.tsServer index construction and static GET
src/content/search/schema.tsSearch Document v2 and structured-content join
src/content/search/facets.tsChapter compatibility plus taxonomy tags / future filters
src/content/search/tokenizer.tsMixed Chinese, English, and pinyin tokenization
src/features/search/client.tsSearch result enhancement pipeline
src/features/search/selection.tsContent selection validation and search query normalization
src/features/search/spotlight.tsSpotlight parameter handling
src/features/search/components/search-dialog.tsxQuery and Chapter scope UI
src/features/search/components/search-spotlight.tsxSearch result spotlight
src/app/[lang]/docs/layout.tsxLocale page tree and navigation layout
src/features/reading/docs-reading-return.tsxCross-document context restoration
src/features/reading/restore.tsScroll restoration on document refresh
src/app/[lang]/docs-source/[...slug]/route.tsMarkdown source endpoint
src/adapters/fumadocs/layout.tsxFumadocs UI i18n
src/features/community/components/guestbook.tsxGiscus locale and theme sync
src/features/community/components/docs-community.tsxDocumentation comment section isolation

Search Spotlight reads transition completion from runtime/navigation and gets the article root through the Fumadocs Adapter instead of inferring state from transition datasets on the root element.

On this page

Discussion

Welcome to share your thoughts and suggestions