From 66f00e7b86cbe2c16c90be8e1fd8764c39ff37d5 Mon Sep 17 00:00:00 2001 From: "Agustin M." Date: Sun, 25 Oct 2026 14:31:11 +0100 Subject: [PATCH] fix(frontend): make docs search work on desktop and mobile (#6715) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend): make docs search work on desktop and mobile Docs search on deerflow.tech was broken twice over: no build step ever generated the Pagefind index that Nextra's loads, and the custom docs header that replaces Nextra's navbar never rendered a search box. - `pnpm build` now runs `search:index` (pagefind 1.5.2, pinned) to write public/_pagefind/ (gitignored). - The landing Header and its mobile sheet take an optional `search` slot; only the docs layout fills it, with localized labels and an aria-label. - Each docs page carries a Pagefind `lang` filter and queries are scoped to it, since the root is fixed and would mix en/zh results. - globals.css restyles the results panel with app tokens because Nextra's variables are unset, lifts it above the mobile sheet, and keeps it tappable; the sheet no longer closes before a result navigates and closes after navigation. Co-Authored-By: Claude Opus 5.5 * docs(frontend): add DESIGN.md describing the shipped design system Records the existing tokens, typography, components, docs (Nextra) overrides, motion, accessibility and copy rules, plus the landing-only decorative exceptions, so UI changes have something to be checked against. Linked from frontend/AGENTS.md. Co-Authored-By: Claude Opus 5.5 * fix(frontend): address Codex review of docs search - Chinese search: the root is fixed, so Pagefind indexed zh pages with the English tokenizer and multi-character words (e.g. 偏好) never matched. scripts/build-search-index.mjs now sets each page's lang from its route before indexing, producing separate en-US and zh-CN indexes, and DocsHtmlLang syncs at runtime (resetting Pagefind on a language switch) so queries hit the right index. - Mobile menu: reset the open state when the route changes instead of comparing against the saved path, so Back no longer reopens the sheet. - Shortcut: stop mounting a second in Nextra's unused mobile menu, which captured Cmd/Ctrl+K and focused an offscreen input at 640-767px. Co-Authored-By: Claude Opus 5.5 * docs(frontend): keep guidance within size budget * fix(frontend): address docs search review feedback Keep Nextra results within the mobile sheet scroll boundary and dismiss same-hash selections by pointer or keyboard. Rebuild Pagefind output without stale fragments and replace UI source-string checks with behavioral coverage. --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Willem Jiang --- frontend/.gitignore | 5 +- frontend/AGENTS.md | 47 +++--- frontend/DESIGN.md | 148 +++++++++++++++++ frontend/README.md | 5 + frontend/package.json | 7 +- frontend/pnpm-lock.yaml | 76 +++++++++ frontend/rstest.config.ts | 3 +- frontend/scripts/build-search-index.mjs | 84 ++++++++++ frontend/src/app/[lang]/docs/layout.tsx | 23 +++ .../src/components/docs/docs-html-lang.tsx | 40 +++++ frontend/src/components/landing/header.tsx | 12 +- .../src/components/landing/mobile-nav.tsx | 74 ++++++++- frontend/src/core/i18n/locales/en-US.ts | 5 + frontend/src/core/i18n/locales/types.ts | 5 + frontend/src/core/i18n/locales/zh-CN.ts | 5 + frontend/src/styles/globals.css | 39 +++++ frontend/tests/e2e/docs-search-mobile.spec.ts | 111 +++++++++++++ frontend/tests/unit/app/docs-search.test.ts | 28 ++++ .../landing/mobile-nav.dom.test.tsx | 151 ++++++++++++++++++ .../unit/scripts/build-search-index.test.ts | 137 ++++++++++++++++ 20 files changed, 977 insertions(+), 28 deletions(-) create mode 100644 frontend/DESIGN.md create mode 100644 frontend/scripts/build-search-index.mjs create mode 100644 frontend/src/components/docs/docs-html-lang.tsx create mode 100644 frontend/tests/e2e/docs-search-mobile.spec.ts create mode 100644 frontend/tests/unit/app/docs-search.test.ts create mode 100644 frontend/tests/unit/components/landing/mobile-nav.dom.test.tsx create mode 100644 frontend/tests/unit/scripts/build-search-index.test.ts diff --git a/frontend/.gitignore b/frontend/.gitignore index c24a8359cf..cd38019705 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -43,4 +43,7 @@ yarn-error.log* *.tsbuildinfo # idea files -.idea \ No newline at end of file +.idea + +# docs search index (generated by `pnpm build`) +/public/_pagefind/ diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 870887b8b8..6589ef9a8d 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -203,18 +203,19 @@ NEXT_PUBLIC_LANGGRAPH_BASE_URL=http://localhost:8001/api Leave these unset for the standard `make dev` / Docker flow, where nginx serves the public `/api/langgraph/*` prefix and rewrites it to Gateway's native `/api/*` routes. -`make build-static` creates a standalone read-only demo and copies `.next/static` -and `public` into the output. In static mode, `core/api/static-response.ts` -resolves Gateway REST reads with the bundled capability catalog and safe -installation projections from existing same-origin `/mock/api` fixtures; writes -and unknown API routes fail locally. -The homepage client counter calls `/github-stars`, outside the Gateway proxy. -That dynamic route reads the server-only `GITHUB_OAUTH_TOKEN` at runtime, caches -GitHub data for one hour, and returns 204 when the count is unavailable. Start -the standalone server from `frontend/` with `node --env-file=.env -.next/standalone/server.js` to load the current credentials. +`make build-static` emits a standalone read-only demo, copying `.next/static` +and `public`. In static mode, `core/api/static-response.ts` serves Gateway reads +from bundled capabilities and safe installation projections in same-origin +`/mock/api` fixtures; writes and unknown routes fail locally. +The homepage counter uses `/github-stars`, outside Gateway. It reads server-only +`GITHUB_OAUTH_TOKEN` at runtime, caches GitHub data for one hour, and returns 204 +when unavailable. From `frontend/`, run `node --env-file=.env +.next/standalone/server.js` to load credentials. -To reach a dev server on anything other than localhost — a LAN address, or a proxied hostname — list the host in `DEER_FLOW_DEV_ALLOWED_ORIGINS` (comma-separated; a full URL is reduced to its host). It feeds Next's `allowedDevOrigins`, which gates `/_next/*`, fonts, and HMR. Without it those requests get a 403 and the page renders server-side but never hydrates, so nothing on it — including the login form — responds. Development only; production builds ignore it. +For non-localhost dev access, set `DEER_FLOW_DEV_ALLOWED_ORIGINS` to comma-separated +hosts (URLs reduce to hosts). Next's `allowedDevOrigins` gates `/_next/*`, fonts +and HMR; otherwise 403s prevent hydration and interactions, including login. +Production ignores this setting. One-time schedule input uses `validZonedLocalToUtcIso` to reject wall times that do not round-trip in the selected timezone. Invalid input emits an empty spec and @@ -230,6 +231,7 @@ round-trip validation must still reject newly entered skipped times. ## Resources +- [DESIGN.md](DESIGN.md) — tokens, type, components, a11y rules; read before UI work - [LangGraph Documentation](https://langchain-ai.github.io/langgraph/) - [LangChain Core Concepts](https://js.langchain.com/docs/concepts) - [TanStack Query Documentation](https://tanstack.com/query/latest) @@ -240,14 +242,19 @@ round-trip validation must still reject newly entered skipped times. Follow `src/` ownership, add types/error handling and unit/E2E coverage, run `pnpm check`, and update this guide for architecture, command, or convention changes. -Route asset budgets are enforced with `pnpm perf:check`. The command measures -`/login` from a normal production build, then builds in static-demo mode for the -fixture-backed workspace routes. It starts the production server on temporary local -ports, measures the unique JavaScript and CSS files referenced by representative -routes, writes the detailed result to `.next/performance-results.json`, and compares -totals with `performance-budgets.json`. Fix route ownership or split points when a -budget fails; do not raise a ceiling without documenting and reviewing the measured -regression. +Docs search: `pnpm build` runs `scripts/build-search-index.mjs`: route-based +language, then replace `public/_pagefind/` (generated). `DocsHtmlLang` syncs +runtime language. Only `Header.search` renders search (`Layout search={null}`). +`MobileNav` uses Headless UI `Portal.Group` inside Radix's scroll boundary; +selections close at the same hash too. Pin behavior with DOM and +`tests/e2e/docs-search-mobile.spec.ts` tests; index tests use temporary files. + +`pnpm perf:check` measures `/login` in a production build and fixture-backed +workspace routes in a static-demo build. It starts servers on temporary local +ports, measures unique JavaScript/CSS for representative routes, writes +`.next/performance-results.json`, and checks `performance-budgets.json`. +Fix route ownership or split points when over budget; document and review +measured regressions before raising ceilings. Chat archive is a thread metadata flag (`deerflow_archived === true`), independent of run status. Sidebar and Chats explicitly request the Gateway's optional @@ -338,7 +345,7 @@ shared-MCP administrator checks remain authoritative; this adds no personal scop -# This is NOT the Next.js you know +## This is NOT the Next.js you know This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md new file mode 100644 index 0000000000..90d6b9fee2 --- /dev/null +++ b/frontend/DESIGN.md @@ -0,0 +1,148 @@ +# DESIGN.md + +The visual system of the DeerFlow frontend, as it ships today. It records what the +code already does so new UI matches it. The source of truth stays in code: +tokens in `src/styles/globals.css`, primitives in `src/components/ui/`. When this +file and the code disagree, fix one of them in the same change. + +## Surfaces + +The app has three surfaces that share one token set: + +| Surface | Routes | Character | +| ------------------ | ---------------------------------- | ---------------------------------------------------------------------------------- | +| Workspace (app UI) | `/workspace/*`, `/login`, `/setup` | Dense, calm, task-focused. Most UI work happens here. | +| Docs | `/[lang]/docs/*` (Nextra 4) | Reading-first. Nextra theme with the landing `Header` in place of Nextra's navbar. | +| Landing and blog | `/`, `/blog/*` | Brand-forward. The only place decorative effects are allowed. | + +## Color + +Colors are shadcn-style semantic tokens in OKLCH, defined on `:root` and `.dark` +in `globals.css` and exposed to Tailwind through `@theme inline` +(`bg-background`, `text-muted-foreground`, `border-border`, ...). + +- **Neutrals carry a warm tint.** Light surfaces use hue 87.47 at chroma 0.0098 + (paper, not pure white). Dark surfaces use hue 106.64 at chroma 0.0036. +- **Primary is pure black in light mode and pure white in dark mode.** There is + no brand hue; contrast does the work. +- **The only chromatic UI color is the focus ring** (`--ring`, blue at hue 255) + plus `--destructive` (red). Charts use `--chart-1..5`. + +| Token | Light | Dark | Use | +| ---------------------- | ---------------------------- | ------------------------------------ | ----------------------------------- | +| `--background` | `oklch(0.9855 0.0098 87.47)` | `oklch(0.24 0.0036 106.64)` | Page | +| `--foreground` | `oklch(0.145 0 0)` | `oklch(0.985 0 0)` | Body text | +| `--card` / `--popover` | `oklch(1 0.0098 87.47)` | `oklch(0.238 / 0.205 0.0036 106.64)` | Raised surfaces, menus, popovers | +| `--primary` | `oklch(0 0 0)` | `oklch(1 0 0)` | Primary buttons, strongest emphasis | +| `--secondary` | `oklch(0.9455 0.0098 87.47)` | `oklch(0.3 0.0036 106.64)` | Secondary buttons, input fills | +| `--muted-foreground` | `oklch(0.556 0 0)` | `oklch(0.708 0 0)` | Secondary text, captions | +| `--accent` | `oklch(0.94 0.0098 87.47)` | `oklch(0.32 0.0036 106.64)` | Hover and active rows | +| `--border` / `--input` | `oklch(0.922 / 0.88 ...)` | `oklch(1 0 0 / 10% / 15%)` | Hairlines, field borders | +| `--ring` | `oklch(0.58 0.18 255)` | `oklch(0.74 0.16 255)` | Focus outlines | +| `--destructive` | `oklch(0.577 0.245 27.3)` | `oklch(0.704 0.191 22.2)` | Errors, delete | + +Rules: + +- Use semantic tokens, never raw hex or Tailwind palette colors, in app and docs + UI. Exceptions that exist today are listed under **Known exceptions**. +- Tints are `color-mix(in oklch, var(--token) N%, transparent)` or Tailwind + opacity modifiers (`bg-secondary/60`), so they follow the theme. +- Theme switching is `next-themes` with `attribute="class"` and system default. + Every new color must work in both modes; check both before shipping. + +## Typography + +- **Sans** (`--font-sans`): the platform UI stack (`ui-sans-serif, system-ui`). + Used for all UI and body text. +- **Serif** (Tailwind `font-serif`): reserved for the **DeerFlow wordmark** and + a few brand moments (header, footer quote, auth pages, workspace header). + Don't use it for UI labels or body text. +- **Mono** (`font-mono`): code, paths, IDs, token counts. +- Dark mode sets `font-weight: 300` on `.dark` to offset light-on-dark bloom. + Don't fight it with per-component weights. +- Sizes are Tailwind's scale. UI text is `text-sm` (14px); body and reading text + is `text-base` (16px); don't set body copy smaller than 14px. + +## Shape, spacing, elevation + +- **Radius**: `--radius` is 10px. `rounded-md` (8px) for controls, `rounded-lg` + (10px) for cards and panels, `rounded-xl`+ only for large containers. Keep + radii consistent within one component. +- **Spacing**: Tailwind's 4px scale. Page gutters are `px-4` on mobile. +- **Width**: `.container-md` steps 40/48/64/80rem; workspace panes use + `--container-width-{xs,sm,md,lg}`. +- **Elevation**: borders before shadows. `shadow-xs` on outline buttons, + `shadow-lg` only on overlays (sheets, dialogs, menus). + +## Components + +Build from `src/components/ui/` (shadcn on Radix) before writing new primitives: +`Button`, `Input`, `Textarea`, `Select`, `Dialog`, `Sheet`, `DropdownMenu`, +`Command`, `Tooltip`, `HoverCard`, `Tabs`, `Switch`, `Badge`, `Card`, `Empty`, +`Skeleton`, `Sonner` (toasts), and more. + +- **Buttons** (`button.tsx`): `default` (primary), `outline`, `secondary`, + `ghost`, `link`, `destructive`. Sizes `sm` (32px), `default` (36px), `lg` + (40px), `icon*`. One `default` button per area; the rest are `outline` or + `ghost`. +- **Icons**: `lucide-react` at `size-4` inside controls. `@radix-ui/react-icons` + only where it is already used (e.g. the GitHub mark). +- **Empty states**: use `Empty` with a sentence that says what to do next, not + only "No items." +- **Loading**: `Skeleton` for layout-shaped loading, a spinner only for short + inline waits. +- **Overlays**: Radix `Sheet`/`Dialog` lock body scrolling and pointer events. + Nested portals must stay inside the modal's allowed content region, above + z-50, with pointer events enabled. Mobile docs search uses Headless UI's + `Portal.Group` targeting the sheet content; z-index alone cannot permit scroll. + See `components/landing/mobile-nav.tsx`. + +## Docs (Nextra) + +- The docs layout renders the landing `Header` instead of Nextra's navbar, and + never renders Nextra's ``, so Nextra's `--nextra-*` variables are unset. + Restyle Nextra parts with app tokens in `globals.css` (see the search panel + rules) instead of defining `--nextra-*`. +- Search is Nextra's `` in the header's `search` slot (desktop) and the + mobile menu sheet (below `sm`), localized via `t.home.search*`, with an + `aria-label`. Matches use a soft foreground tint plus bold, not inverted + blocks. +- Known gap: below 768px there is no control to open the docs sidebar. + +## Motion + +- Short, functional transitions: `transition-colors` on hover, Radix + enter/exit animations, `tw-animate-css` utilities. Durations 150 to 300ms. +- Named animations live in `@theme` in `globals.css` (`fade-in`, `fade-in-up`, + `loading-bar`, `skeleton-entrance`, ...). Reuse them before adding new ones. +- Respect `prefers-reduced-motion` for anything that loops or moves content. + +## Accessibility + +- Every input has a visible label or an `aria-label`; placeholders are hints, + not labels. +- Icon-only buttons need `aria-label` (e.g. `aria-label="Open menu"`). +- Keep the visible focus ring (`focus-visible:ring-ring/50`); never remove + outlines without a replacement. +- Touch targets in mobile-only UI are at least 44px tall. +- Body text contrast is at least 4.5:1 in both themes; `muted-foreground` is for + secondary text only. + +## Copy + +- Utility language in the app: say what the area is or what the user can do. +- Every user-visible string goes through `src/core/i18n/locales/` with both + `en-US` and `zh-CN` entries. +- Use the typographic ellipsis (`…`) in placeholders and loading text. + +## Known exceptions + +Landing-only decoration that does not follow the rules above. Don't copy these +into app or docs UI: + +- `Section` headings use a white-to-gray gradient text (`bg-clip-text`), and + `.golden-text` is a gold gradient. +- `.ambilight` is a rainbow glow, and the header's Star on GitHub button has a + pink-to-violet blur behind it (`#ff80b5` to `#9089fc`). +- `galaxy`, `aurora-text`, `flickering-grid`, `magic-bento`, `spotlight-card` + and `shine-border` in `components/ui/` are landing effects. diff --git a/frontend/README.md b/frontend/README.md index ed89081da9..5aa4fc3c81 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -68,6 +68,11 @@ pnpm build pnpm start ``` +Production builds recreate `public/_pagefind/` from the current documentation, +with separate English and Chinese indexes. The index step replaces its generated +output directory to prevent old assets accumulating. Mobile docs search results scroll inside +the menu and close it after selection, including the section already open. + ## Site Map ``` diff --git a/frontend/package.json b/frontend/package.json index 69b6bc6131..ae3c2aa917 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,7 @@ "scripts": { "catalog:sync": "node scripts/sync-capability-catalog.mjs", "demo:save": "node scripts/save-demo.js", - "build": "next build", + "build": "next build && pnpm run search:index", "check": "eslint . --ext .ts,.tsx && tsc --noEmit", "dev": "node scripts/dev.mjs", "format": "prettier --check .", @@ -14,7 +14,8 @@ "lint": "eslint . --ext .ts,.tsx", "lint:fix": "eslint . --ext .ts,.tsx --fix", "perf:check": "node scripts/measure-route-assets.mjs --check", - "preview": "next build && next start", + "preview": "pnpm run build && next start", + "search:index": "node scripts/build-search-index.mjs", "start": "next start", "test": "rstest", "test:e2e": "playwright test", @@ -28,6 +29,7 @@ "@codemirror/lang-markdown": "^6.5.0", "@codemirror/lang-python": "^6.2.1", "@codemirror/language-data": "^6.5.2", + "@headlessui/react": "^2.2.9", "@langchain/core": "^1.1.15", "@langchain/langgraph-sdk": "^1.5.3", "@radix-ui/react-avatar": "^1.1.11", @@ -118,6 +120,7 @@ "eslint": "^9.23.0", "eslint-config-next": "^15.2.3", "happy-dom": "^21.0.4", + "pagefind": "1.5.2", "postcss": "^8.5.18", "prettier": "^3.5.3", "prettier-plugin-tailwindcss": "^0.6.11", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index da41b3a7b4..502fc87a2a 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -34,6 +34,9 @@ importers: '@codemirror/language-data': specifier: ^6.5.2 version: 6.5.2 + '@headlessui/react': + specifier: ^2.2.9 + version: 2.2.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@langchain/core': specifier: ^1.1.15 version: 1.1.20(@opentelemetry/api@1.9.0) @@ -299,6 +302,9 @@ importers: happy-dom: specifier: ^21.0.4 version: 21.0.4 + pagefind: + specifier: 1.5.2 + version: 1.5.2 postcss: specifier: ^8.5.18 version: 8.5.25 @@ -1215,6 +1221,41 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} + '@pagefind/darwin-arm64@1.5.2': + resolution: {integrity: sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==} + cpu: [arm64] + os: [darwin] + + '@pagefind/darwin-x64@1.5.2': + resolution: {integrity: sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw==} + cpu: [x64] + os: [darwin] + + '@pagefind/freebsd-x64@1.5.2': + resolution: {integrity: sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA==} + cpu: [x64] + os: [freebsd] + + '@pagefind/linux-arm64@1.5.2': + resolution: {integrity: sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw==} + cpu: [arm64] + os: [linux] + + '@pagefind/linux-x64@1.5.2': + resolution: {integrity: sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA==} + cpu: [x64] + os: [linux] + + '@pagefind/windows-arm64@1.5.2': + resolution: {integrity: sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g==} + cpu: [arm64] + os: [win32] + + '@pagefind/windows-x64@1.5.2': + resolution: {integrity: sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg==} + cpu: [x64] + os: [win32] + '@playwright/test@1.62.1': resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} engines: {node: '>=20'} @@ -4855,6 +4896,10 @@ packages: package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + pagefind@1.5.2: + resolution: {integrity: sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q==} + hasBin: true + pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} @@ -7016,6 +7061,27 @@ snapshots: '@opentelemetry/api@1.9.0': {} + '@pagefind/darwin-arm64@1.5.2': + optional: true + + '@pagefind/darwin-x64@1.5.2': + optional: true + + '@pagefind/freebsd-x64@1.5.2': + optional: true + + '@pagefind/linux-arm64@1.5.2': + optional: true + + '@pagefind/linux-x64@1.5.2': + optional: true + + '@pagefind/windows-arm64@1.5.2': + optional: true + + '@pagefind/windows-x64@1.5.2': + optional: true + '@playwright/test@1.62.1': dependencies: playwright: 1.62.1 @@ -11304,6 +11370,16 @@ snapshots: package-manager-detector@1.6.0: {} + pagefind@1.5.2: + optionalDependencies: + '@pagefind/darwin-arm64': 1.5.2 + '@pagefind/darwin-x64': 1.5.2 + '@pagefind/freebsd-x64': 1.5.2 + '@pagefind/linux-arm64': 1.5.2 + '@pagefind/linux-x64': 1.5.2 + '@pagefind/windows-arm64': 1.5.2 + '@pagefind/windows-x64': 1.5.2 + pako@0.2.9: {} papaparse@5.7.0: {} diff --git a/frontend/rstest.config.ts b/frontend/rstest.config.ts index a67b270ffc..401f9bb3d3 100644 --- a/frontend/rstest.config.ts +++ b/frontend/rstest.config.ts @@ -15,7 +15,8 @@ const shared = { output: { // Streamdown imports KaTeX CSS as a side effect. Bundle these packages so // Rsbuild processes that CSS import instead of Node trying to load it. - bundleDependencies: ["streamdown", "katex"], + // Nextra's client icons also use extensionless imports that need bundling. + bundleDependencies: ["streamdown", "katex", "nextra"], }, }; diff --git a/frontend/scripts/build-search-index.mjs b/frontend/scripts/build-search-index.mjs new file mode 100644 index 0000000000..ae86149066 --- /dev/null +++ b/frontend/scripts/build-search-index.mjs @@ -0,0 +1,84 @@ +#!/usr/bin/env node + +// Builds the Pagefind index that Nextra's docs loads from +// /_pagefind/. The root layout renders a fixed , so the Pagefind +// CLI would index every page as English and leave Chinese text unsegmented. +// Rewrite each page's lang from its /{lang}/ route prefix first, so Pagefind +// writes one correctly tokenized index per language. + +import { readdir, readFile, rm } from "node:fs/promises"; +import path from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; + +import * as pagefind from "pagefind"; + +const SITE_DIR = ".next/server/app"; +const OUTPUT_DIR = "public/_pagefind"; +const LOCALE_BY_PREFIX = { en: "en-US", zh: "zh-CN" }; + +async function* htmlFiles(dir) { + for (const entry of await readdir(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + yield* htmlFiles(fullPath); + } else if (entry.name.endsWith(".html")) { + yield fullPath; + } + } +} + +export function localeForPage(relativePath) { + const prefix = relativePath.split(/[/\\.]/)[0]; + return LOCALE_BY_PREFIX[prefix]; +} + +export function withHtmlLang(html, locale) { + return html.replace( + /]*?)\blang="[^"]*"/, + ` { + console.error(error); + process.exitCode = 1; + }); +} diff --git a/frontend/src/app/[lang]/docs/layout.tsx b/frontend/src/app/[lang]/docs/layout.tsx index 292fefb8ca..d903b10bbe 100644 --- a/frontend/src/app/[lang]/docs/layout.tsx +++ b/frontend/src/app/[lang]/docs/layout.tsx @@ -1,13 +1,16 @@ import "katex/dist/katex.min.css"; +import { Search } from "nextra/components"; import { getPageMap } from "nextra/page-map"; import { Layout } from "nextra-theme-docs"; +import { DocsHtmlLang } from "@/components/docs/docs-html-lang"; import { DocsLanguageProvider } from "@/components/docs/docs-language-context"; import { buildLocalizedDocsPageMap } from "@/components/docs/docs-page-map"; import { Footer } from "@/components/landing/footer"; import { Header } from "@/components/landing/header"; import { getLocaleByLang } from "@/core/i18n/locale"; +import { getI18n } from "@/core/i18n/server"; import "nextra-theme-docs/style.css"; const i18n = [ @@ -20,6 +23,20 @@ export default async function DocLayout({ children, params }) { const locale = getLocaleByLang(lang); const pages = await getPageMap(`/${lang}`); const pageMap = buildLocalizedDocsPageMap(`/${lang}/docs`, pages); + const { t } = await getI18n(locale); + // Pagefind keeps one index per language (see scripts/build-search-index.mjs); + // the lang filter also scopes queries if has not synced yet. + const search = ( + + ); return ( @@ -29,14 +46,20 @@ export default async function DocLayout({ children, params }) { className="sticky max-w-full px-10" homeURL="/" locale={locale} + search={search} /> } pageMap={pageMap} + // Search lives in the custom Header; Nextra's own mobile menu is never + // shown, and a second instance would steal the Cmd/Ctrl+K shortcut. + search={null} docsRepositoryBase="https://github.com/bytedance/deer-flow/tree/main/frontend" footer={