feat(docs): add static generation and search indexes
This commit is contained in:
@@ -9,18 +9,8 @@ import { messages as searchMessagesEnUS } from "@workspace/search/locales/en-US"
|
||||
import { messages as searchMessagesZhHans } from "@workspace/search/locales/zh-Hans"
|
||||
import React from "react"
|
||||
|
||||
import {
|
||||
getDocHref,
|
||||
getDocPackages,
|
||||
getDocSearchText,
|
||||
getDocSectionLabel,
|
||||
} from "../content/registry"
|
||||
import type {
|
||||
DocLocale,
|
||||
DocPackage,
|
||||
DocPage,
|
||||
DocTocItem,
|
||||
} from "../content/types"
|
||||
import { loadDocSearchIndex } from "../content/search-index.client"
|
||||
import type { DocLocale, DocSearchEntry } from "../content/types"
|
||||
import { useLocale } from "../lib/locale"
|
||||
|
||||
const searchCatalogs = {
|
||||
@@ -57,69 +47,73 @@ export function DocsSearchProvider({
|
||||
}
|
||||
|
||||
function createDocsSearchAdapter(locale: DocLocale): SearchAdapter {
|
||||
const docPackages = getDocPackages(locale)
|
||||
|
||||
return {
|
||||
async search({ query, signal }) {
|
||||
const normalizedQuery = normalize(query)
|
||||
if (!normalizedQuery) return { groups: [] }
|
||||
|
||||
const index = await loadDocSearchIndex(locale)
|
||||
if (signal.aborted) return { groups: [] }
|
||||
|
||||
const terms = normalizedQuery.split(/\s+/).filter(Boolean)
|
||||
const groups = docPackages.flatMap((docPackage) => {
|
||||
const items = docPackage.pages
|
||||
.map((page) => ({
|
||||
item: createResultItem(docPackage, page, locale),
|
||||
score: getMatchScore(
|
||||
normalizedQuery,
|
||||
terms,
|
||||
docPackage.label,
|
||||
page
|
||||
),
|
||||
}))
|
||||
.filter((result) => result.score >= 0)
|
||||
.sort(
|
||||
const groups = new Map<
|
||||
string,
|
||||
{
|
||||
id: string
|
||||
items: Array<{ item: SearchResultItem; score: number }>
|
||||
label: string
|
||||
total: number
|
||||
}
|
||||
>()
|
||||
|
||||
for (const entry of index) {
|
||||
const score = getMatchScore(normalizedQuery, terms, entry)
|
||||
if (score < 0) continue
|
||||
|
||||
const group = groups.get(entry.packageKey) ?? {
|
||||
id: entry.packageKey,
|
||||
items: [],
|
||||
label: entry.packageLabel,
|
||||
total: 0,
|
||||
}
|
||||
group.items.push({ item: createResultItem(entry), score })
|
||||
group.total += 1
|
||||
groups.set(entry.packageKey, group)
|
||||
}
|
||||
|
||||
const results = Array.from(groups.values(), (group) => ({
|
||||
id: group.id,
|
||||
label: group.label,
|
||||
total: group.total,
|
||||
items: group.items
|
||||
.toSorted(
|
||||
(left, right) =>
|
||||
right.score - left.score ||
|
||||
left.item.title.localeCompare(right.item.title, locale)
|
||||
)
|
||||
.map((result) => result.item)
|
||||
|
||||
return items.length > 0
|
||||
? [
|
||||
{
|
||||
id: docPackage.key,
|
||||
label: docPackage.label,
|
||||
items,
|
||||
total: items.length,
|
||||
},
|
||||
]
|
||||
: []
|
||||
})
|
||||
.map((result) => result.item),
|
||||
}))
|
||||
|
||||
if (signal.aborted) return { groups: [] }
|
||||
return { groups }
|
||||
return { groups: results }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createResultItem(
|
||||
docPackage: DocPackage,
|
||||
page: DocPage,
|
||||
locale: DocLocale
|
||||
): SearchResultItem {
|
||||
function createResultItem(entry: DocSearchEntry): SearchResultItem {
|
||||
return {
|
||||
id: `${page.section}/${page.slug}`,
|
||||
title: page.title,
|
||||
description: page.description,
|
||||
id: entry.id,
|
||||
title: entry.title,
|
||||
description: entry.description,
|
||||
keywords: [
|
||||
docPackage.label,
|
||||
getDocSectionLabel(page.section, locale),
|
||||
...page.toc.flatMap(flattenTocTitles),
|
||||
getDocSearchText(page),
|
||||
entry.packageLabel,
|
||||
entry.sectionLabel,
|
||||
...entry.toc,
|
||||
entry.body,
|
||||
],
|
||||
meta: [getDocSectionLabel(page.section, locale)],
|
||||
meta: [entry.sectionLabel],
|
||||
payload: {
|
||||
href: getDocHref(docPackage, page),
|
||||
href: entry.href,
|
||||
} satisfies DocsSearchPayload,
|
||||
}
|
||||
}
|
||||
@@ -127,14 +121,13 @@ function createResultItem(
|
||||
function getMatchScore(
|
||||
query: string,
|
||||
terms: readonly string[],
|
||||
packageLabel: string,
|
||||
page: DocPage
|
||||
entry: DocSearchEntry
|
||||
) {
|
||||
const title = normalize(page.title)
|
||||
const description = normalize(page.description)
|
||||
const packageName = normalize(packageLabel)
|
||||
const toc = normalize(page.toc.flatMap(flattenTocTitles).join(" "))
|
||||
const body = normalize(getDocSearchText(page))
|
||||
const title = normalize(entry.title)
|
||||
const description = normalize(entry.description)
|
||||
const packageName = normalize(entry.packageLabel)
|
||||
const toc = normalize(entry.toc.join(" "))
|
||||
const body = normalize(entry.body)
|
||||
const searchableText = [packageName, title, description, toc, body].join(" ")
|
||||
|
||||
if (!terms.every((term) => searchableText.includes(term))) return -1
|
||||
@@ -147,15 +140,11 @@ function getMatchScore(
|
||||
else if (packageName.includes(query)) score += 20
|
||||
if (description.includes(query)) score += 15
|
||||
if (toc.includes(query)) score += 10
|
||||
if (page.section === "overview") score += 2
|
||||
if (entry.id === "overview/overview") score += 2
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
function flattenTocTitles(item: DocTocItem): readonly string[] {
|
||||
return [item.title, ...(item.items?.flatMap(flattenTocTitles) ?? [])]
|
||||
}
|
||||
|
||||
function normalize(value: string) {
|
||||
return value.normalize("NFKC").toLocaleLowerCase().trim()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user