mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-27 21:50:36 +00:00
* 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 <Search> 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 <html lang> is fixed and would mix en/zh results. - globals.css restyles the results panel with app tokens because Nextra's <Head> 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * fix(frontend): address Codex review of docs search - Chinese search: the root <html lang> 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 <html lang> 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 <Search> 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
138 lines
4.8 KiB
TypeScript
138 lines
4.8 KiB
TypeScript
import {
|
|
mkdtemp,
|
|
mkdir,
|
|
readFile,
|
|
readdir,
|
|
rm,
|
|
writeFile,
|
|
} from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, rs } from "@rstest/core";
|
|
|
|
const api = rs.hoisted(() => ({
|
|
createIndex: rs.fn(),
|
|
addHTMLFile: rs.fn(),
|
|
writeFiles: rs.fn(),
|
|
close: rs.fn(),
|
|
}));
|
|
|
|
rs.mock("pagefind", () => ({
|
|
createIndex: api.createIndex,
|
|
close: api.close,
|
|
}));
|
|
|
|
import {
|
|
buildSearchIndex,
|
|
localeForPage,
|
|
withHtmlLang,
|
|
} from "../../../scripts/build-search-index.mjs";
|
|
|
|
let root: string;
|
|
let siteDir: string;
|
|
let outputDir: string;
|
|
|
|
beforeEach(async () => {
|
|
root = await mkdtemp(path.join(tmpdir(), "docs-search-index-"));
|
|
siteDir = path.join(root, "site");
|
|
outputDir = path.join(root, "public", "_pagefind");
|
|
await mkdir(path.join(siteDir, "en", "docs"), { recursive: true });
|
|
await mkdir(path.dirname(outputDir), { recursive: true });
|
|
api.createIndex.mockReset().mockResolvedValue({
|
|
index: { addHTMLFile: api.addHTMLFile, writeFiles: api.writeFiles },
|
|
errors: [],
|
|
});
|
|
api.addHTMLFile.mockReset().mockResolvedValue({ errors: [] });
|
|
api.writeFiles.mockReset().mockImplementation(async ({ outputPath }) => {
|
|
await mkdir(outputPath, { recursive: true });
|
|
// Like Pagefind, only write new hashed assets; leave previous files alone.
|
|
await writeFile(
|
|
path.join(outputPath, `fragment-${api.writeFiles.mock.calls.length}`),
|
|
"indexed content",
|
|
);
|
|
return { errors: [] };
|
|
});
|
|
api.close.mockReset().mockResolvedValue(undefined);
|
|
rs.spyOn(console, "log").mockImplementation(() => undefined);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await rm(root, { recursive: true, force: true });
|
|
rs.restoreAllMocks();
|
|
});
|
|
|
|
describe("docs search index", () => {
|
|
it("replaces old generated assets on a second build and preserves other public files", async () => {
|
|
const oldPage = path.join(siteDir, "en", "docs", "old.html");
|
|
const logo = path.join(root, "public", "logo.txt");
|
|
await writeFile(oldPage, '<html lang="en-US"><body>Old page</body></html>');
|
|
await writeFile(logo, "unrelated public asset");
|
|
await buildSearchIndex({ siteDir, outputDir });
|
|
expect(await readdir(outputDir)).toEqual(["fragment-1"]);
|
|
|
|
await rm(oldPage);
|
|
await writeFile(
|
|
path.join(siteDir, "en", "docs", "renamed.html"),
|
|
'<html lang="en-US"><body>Renamed page</body></html>',
|
|
);
|
|
await buildSearchIndex({ siteDir, outputDir });
|
|
|
|
expect(await readdir(outputDir)).toEqual(["fragment-2"]);
|
|
expect(await readFile(logo, "utf8")).toBe("unrelated public asset");
|
|
expect(api.addHTMLFile.mock.calls.map(([page]) => page.url)).toEqual([
|
|
"/en/docs/old.html",
|
|
"/en/docs/renamed.html",
|
|
]);
|
|
expect(api.close).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("indexes nested HTML with the route language and leaves other assets out", async () => {
|
|
await mkdir(path.join(siteDir, "zh", "docs"), { recursive: true });
|
|
await writeFile(
|
|
path.join(siteDir, "zh", "docs", "sandbox.html"),
|
|
'<html lang="en-US" class="theme"><body>沙箱</body></html>',
|
|
);
|
|
await writeFile(path.join(siteDir, "en", "docs", "image.png"), "image");
|
|
await buildSearchIndex({ siteDir, outputDir });
|
|
expect(api.addHTMLFile).toHaveBeenCalledExactlyOnceWith({
|
|
url: "/zh/docs/sandbox.html",
|
|
content: '<html lang="zh-CN" class="theme"><body>沙箱</body></html>',
|
|
});
|
|
});
|
|
|
|
it("uses one tokenizer per supported route language", () => {
|
|
expect(localeForPage("zh/docs/harness/memory.html")).toBe("zh-CN");
|
|
expect(localeForPage("zh.html")).toBe("zh-CN");
|
|
expect(localeForPage("en/docs/harness/memory.html")).toBe("en-US");
|
|
expect(localeForPage("blog/posts.html")).toBeUndefined();
|
|
expect(withHtmlLang('<html lang="en-US" class="theme">', "zh-CN")).toBe(
|
|
'<html lang="zh-CN" class="theme">',
|
|
);
|
|
});
|
|
|
|
it("keeps the previous index if reading/indexing pages fails, and closes Pagefind", async () => {
|
|
await mkdir(outputDir, { recursive: true });
|
|
await writeFile(path.join(outputDir, "pagefind.js"), "previous index");
|
|
await writeFile(path.join(siteDir, "en", "docs", "page.html"), "page");
|
|
api.addHTMLFile.mockResolvedValueOnce({ errors: ["indexing failed"] });
|
|
|
|
await expect(buildSearchIndex({ siteDir, outputDir })).rejects.toThrow(
|
|
"indexing failed",
|
|
);
|
|
expect(await readFile(path.join(outputDir, "pagefind.js"), "utf8")).toBe(
|
|
"previous index",
|
|
);
|
|
expect(api.writeFiles).not.toHaveBeenCalled();
|
|
expect(api.close).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("reports publication errors and closes Pagefind", async () => {
|
|
api.writeFiles.mockResolvedValueOnce({ errors: ["write failed"] });
|
|
await expect(buildSearchIndex({ siteDir, outputDir })).rejects.toThrow(
|
|
"write failed",
|
|
);
|
|
expect(api.close).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|