mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-24 20:09:02 +00:00
fix(docs): make search work on the static site (client-side flexsearch)
The search dialog used `type: 'hanzo'`, POSTing to a remote auth-gated API a public static site can't call — every query silently returned empty. The `/api/search` route also froze to the force-static empty-query `[]`. - route: export `staticGET` (the whole flexsearch index) instead of the dynamic GET; tokenize `forward` (prefix / as-you-type) over titles + section headings only. A full-body forward index exceeds V8's max string length and OOMs the export; headings-only is ~20MB / 1.7MB gzipped. - dialog: `type: 'flexsearch-static'` from /api/search — no server, no auth. - core: wire the missing `flexsearch-static` case into useDocsSearch; re-export buildIndexDefault so the route can tag pages (All/Services/SDKs/API/Products). Verified on the built out/api/search: agents 85, authentication 113, getting started 40, prefix "deplo"->/docs/deploy; all five filters return results.
This commit is contained in:
@@ -1,7 +1,64 @@
|
||||
import { source } from '@/lib/source';
|
||||
import { flexsearchFromSource } from '@hanzo/docs-core/search/flexsearch';
|
||||
import { buildIndexDefault, flexsearchFromSource } from '@hanzo/docs-core/search/flexsearch';
|
||||
import { getSection } from '@/lib/source/navigation';
|
||||
|
||||
export const { GET } = flexsearchFromSource(source);
|
||||
// The search dialog's "Products" filter covers Zen LM, Chat, MCP, Dev and ZAP.
|
||||
// getSection keeps those as distinct sections, so we additionally tag their
|
||||
// pages `products` for that one filter. The other filters (Services/SDKs/API)
|
||||
// match the single section tag as before — flexsearch tags are a set, so a
|
||||
// page can carry both its section and `products`.
|
||||
const PRODUCT_SECTIONS = new Set(['chat', 'llm', 'mcp', 'dev', 'zap']);
|
||||
|
||||
// Static flexsearch index for CF Pages static export.
|
||||
export const dynamic = 'force-static';
|
||||
function sectionTags(slug: string | undefined): string | string[] {
|
||||
const section = getSection(slug);
|
||||
return PRODUCT_SECTIONS.has(section) ? [section, 'products'] : section;
|
||||
}
|
||||
|
||||
// Client-side static search for the static export (served by hanzoai/static).
|
||||
//
|
||||
// `staticGET` serializes the flexsearch index to /api/search; the browser
|
||||
// (search dialog, `type: 'flexsearch-static'`) downloads it once and searches
|
||||
// locally — no server, no auth, works on a static host. This replaces the old
|
||||
// `GET` (dynamic search handler) which, under `dynamic = 'force-static'`, froze
|
||||
// to the empty-query response `[]`, so every search returned nothing.
|
||||
//
|
||||
// Index shape is tuned for this corpus (~1.7k docs):
|
||||
// - `tokenize: 'forward'` gives prefix / search-as-you-type matching.
|
||||
// - We index titles, descriptions and section headings, NOT body paragraphs
|
||||
// (`contents: []`). Indexing full bodies with a prefix tokenizer produces a
|
||||
// serialized index that exceeds V8's max string length and OOMs the build;
|
||||
// headings-only keeps it at ~20MB (~1.7MB gzipped) while still matching the
|
||||
// titles/headings users actually search for. Deep body text is reachable
|
||||
// via the on-page anchors those headings link to.
|
||||
//
|
||||
// Tags mirror /static.json (getSection over the top-level slug) so the
|
||||
// All/Services/SDKs/API filters narrow correctly.
|
||||
export const { staticGET: GET } = flexsearchFromSource(source, {
|
||||
document: { tokenize: 'forward' },
|
||||
async buildIndex(page) {
|
||||
const tag = sectionTags(page.slugs[0]);
|
||||
|
||||
// OpenAPI pages carry no MDX structured data; index title/description only.
|
||||
// (In static-export mode they aren't generated at all, so this is a no-op
|
||||
// there — it keeps `next dev`, where specs are present, from throwing.)
|
||||
if (page.type === 'openapi') {
|
||||
return {
|
||||
id: page.url,
|
||||
url: page.url,
|
||||
title: page.data.title ?? page.url,
|
||||
description: page.data.description,
|
||||
tag,
|
||||
structuredData: { headings: [], contents: [] },
|
||||
};
|
||||
}
|
||||
|
||||
const base = await buildIndexDefault(page);
|
||||
return {
|
||||
...base,
|
||||
tag,
|
||||
structuredData: { headings: base.structuredData.headings, contents: [] },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
@@ -22,7 +22,6 @@ import { cn } from '@/lib/cn';
|
||||
import { useTreeContext } from '@hanzo/docs/ui/contexts/tree';
|
||||
import type { Item, Node } from '@hanzo/docs/core/page-tree';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { publishableKey, searchEndpoint, searchBackend, searchIndex } from '@/lib/hanzo/client';
|
||||
|
||||
const items = [
|
||||
{
|
||||
@@ -55,11 +54,8 @@ export default function CustomSearchDialog(props: SharedProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [tag, setTag] = useState<string | undefined>();
|
||||
const { search, setSearch, query } = useDocsSearch({
|
||||
type: 'hanzo',
|
||||
apiKey: publishableKey,
|
||||
endpoint: searchEndpoint,
|
||||
backend: searchBackend,
|
||||
index: searchIndex,
|
||||
type: 'flexsearch-static',
|
||||
from: '/api/search',
|
||||
tag,
|
||||
});
|
||||
const { full } = useTreeContext();
|
||||
|
||||
@@ -147,6 +147,15 @@ export function useDocsSearch(
|
||||
client = oramaStaticClient(clientRest);
|
||||
break;
|
||||
}
|
||||
case 'flexsearch-static': {
|
||||
const res = (promiseMap[clientRest.type] ??=
|
||||
import('./client/flexsearch-static')) as Promise<
|
||||
typeof import('./client/flexsearch-static')
|
||||
>;
|
||||
const { flexsearchStaticClient } = use(res);
|
||||
client = flexsearchStaticClient(clientRest);
|
||||
break;
|
||||
}
|
||||
case 'hanzo': {
|
||||
const res = (promiseMap[clientRest.type] ??= import('./client/hanzo')) as Promise<
|
||||
typeof import('./client/hanzo')
|
||||
|
||||
@@ -8,6 +8,10 @@ import type { Awaitable } from '@/types';
|
||||
import type { I18nConfig } from '@/i18n';
|
||||
import { createDocument, search, type Doc } from './flexsearch/utils';
|
||||
|
||||
// Re-exported so apps can wrap the default page->index builder (e.g. to attach
|
||||
// a `tag` for filtered client-side search) instead of reimplementing it.
|
||||
export { buildIndexDefault } from './server/build-index';
|
||||
|
||||
export type Index = SharedIndex;
|
||||
export interface IndexWithLocale extends Index {
|
||||
locale: string;
|
||||
|
||||
Reference in New Issue
Block a user