From 63e1faa63ce2b3299113de6ddcda8b2d2ddfcd9f Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 14 Aug 2026 04:19:47 +0000 Subject: [PATCH] fix(search): scope the results to the version being read (#512) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #146. The published site runs pagefind, not docsearch: `PUBLIC_DOCSEARCH_APP_ID` and `PUBLIC_DOCSEARCH_API_KEY` are not set, so `astro.config.mjs` falls back to `pagefind: true`, and a page of docs.gitea.com ships starlight's `#starlight__search` with the `/pagefind/` bundle. The version scoping we have is only implemented for docsearch (`docsearch:product` / `docsearch:version` meta tags plus `facetFilters` in `src/config/docsearch.ts`), so nothing consumes it today. Pagefind builds one index for the whole site, partitioned only by the html language, and nothing tags the pages with a version, so a search started in the 1.23 docs answers with every version of the same page and the latest one usually wins. That is what the issue reports; the original cause (the docusaurus search plugin) is gone, this one replaced it. **What this does** - `src/components/MarkdownContent.astro` wraps the content in two `data-pagefind-filter` elements, `product` and `version`, so the filters land in the index. One filter per element on purpose: pagefind reads the whole attribute as a single `name:value` pair, a comma separated value ends up as one filter named `product` with the value `docs,version:1.26`. - The same scope is written to a `gitea:search-filters` meta tag next to the docsearch ones (`src/lib/search.ts`). - Starlight builds the search ui from a build time configuration and has no option for per page filters, so its `@pagefind/default-ui` import is redirected to a subclass in `src/lib/pagefind-ui.ts` by the `gitea-pagefind-filters` plugin in `astro.config.mjs`, the same approach the `gitea-openapi-overview` plugin already uses. The subclass selects the filters of the page the modal was opened on. - A "Search all versions" checkbox below the search input drops the filters again and labels each result with the version (and the product for the api and the runner) it comes from, so the duplicates are distinguishable. Translated into 简体中文 and 繁體中文. - Pagefind's own filter panel is hidden, it is redundant next to the checkbox and starlight does not style it. The checkbox gets its native rendering back, pagefind resets everything it renders with `all: unset`. - A `gitea-pagefind-filters-check` integration fails the build if the redirect did not run, so a starlight upgrade that moves the import cannot silently bring back an unscoped search. `@pagefind/default-ui` is added to `sites/docs/package.json`; it was only reachable as a transitive dependency of starlight, and the version is the one already in the lockfile. **Verification** Built the whole site and drove the search with a headless browser against `astro preview` (the search does not run in `pnpm dev`): | Page | Query | Results | | --- | --- | --- | | `/1.23/usage/actions/comparison/` | email | only `/1.23/...` | | `/usage/actions/overview/` | email | only the version served at the root | | `/runner/registration/` | label | only `/runner/...` | | `/api/operations/tags/issue/` | label | only `/api/...` | | `/zh-cn/1.26/usage/actions/overview/` | email | only `/zh-cn/1.26/...` | | `/1.23/...` with the checkbox ticked | email | every version, results labelled `... · API 1.25`, `... · next` | `pnpm check` passes. The docsearch path is unchanged: when the credentials are set the plugin takes over, the redirect does not run and the check integration skips. Preview: https://pr-512.docs-gitea-com.pages.dev Reviewed-on: https://gitea.com/gitea/docs/pulls/512 --- pnpm-lock.yaml | 3 + sites/docs/README.md | 19 ++++ sites/docs/astro.config.mjs | 38 +++++++ sites/docs/package.json | 1 + .../docs/src/components/MarkdownContent.astro | 14 ++- sites/docs/src/config/strings.ts | 5 + sites/docs/src/lib/pagefind-ui.ts | 104 ++++++++++++++++++ sites/docs/src/lib/search-filters.ts | 39 +++++++ sites/docs/src/lib/search.ts | 18 ++- sites/docs/src/pagefind-ui.d.ts | 12 ++ sites/docs/src/styles/custom.css | 28 +++++ 11 files changed, 277 insertions(+), 4 deletions(-) create mode 100644 sites/docs/src/lib/pagefind-ui.ts create mode 100644 sites/docs/src/lib/search-filters.ts create mode 100644 sites/docs/src/pagefind-ui.d.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 079203b2..8d5b7943 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,6 +31,9 @@ importers: '@gitea-docs/content-loader': specifier: workspace:* version: link:../../packages/content-loader + '@pagefind/default-ui': + specifier: ^1.5.2 + version: 1.5.2 astro: specifier: 7.2.0 version: 7.2.0(@astrojs/markdown-remark@7.2.2(supports-color@7.2.0))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(yaml@2.9.0) diff --git a/sites/docs/README.md b/sites/docs/README.md index ded4a3d8..8a9a0107 100644 --- a/sites/docs/README.md +++ b/sites/docs/README.md @@ -101,6 +101,25 @@ into facets and `src/config/docsearch.ts` filters the search on the product, version and language being read. `cloudflare/docsearch-crawler.json` holds the crawler configuration and only indexes the versions people read. +Pagefind builds one index for the whole site, so the same scope has to be +applied on its side as well, otherwise a search started in the 1.23 docs answers +with the pages of the latest release: + +- `src/components/MarkdownContent.astro` wraps the content in the + `data-pagefind-filter` elements that put `product` and `version` into the + index. One filter per element, pagefind reads the whole attribute as a single + `name:value` pair. +- the same scope is written to the `gitea:search-filters` meta tag by + `src/lib/search.ts`. +- starlight builds the search ui with a build time configuration and has no + option for per page filters, so its `@pagefind/default-ui` import is + redirected to the subclass in `src/lib/pagefind-ui.ts` by the + `gitea-pagefind-filters` plugin in `astro.config.mjs`. It selects the filters + of the page the modal was opened on and adds the "Search all versions" + checkbox, which drops them again and labels the results with their version. + A starlight upgrade that moves the import fails the build, the + `gitea-pagefind-filters-check` integration verifies that the redirect ran. + ## Deployment `cloudflare/_headers` and `cloudflare/_redirects` are copied next to the build diff --git a/sites/docs/astro.config.mjs b/sites/docs/astro.config.mjs index 626ea49e..a4122dea 100644 --- a/sites/docs/astro.config.mjs +++ b/sites/docs/astro.config.mjs @@ -18,6 +18,11 @@ const useDocSearch = Boolean( process.env.PUBLIC_DOCSEARCH_APP_ID && process.env.PUBLIC_DOCSEARCH_API_KEY, ); +// set by the `gitea-pagefind-filters` plugin below, checked after the build so +// a starlight upgrade that moves the import fails loudly instead of silently +// serving an unscoped search again +let pagefindUiRedirected = false; + export default defineConfig({ site: 'https://docs.gitea.com', trailingSlash: 'always', @@ -44,6 +49,26 @@ export default defineConfig({ return null; }, }, + { + // Pagefind indexes every version into one index and starlight builds + // the search ui with a build time configuration, so a search cannot be + // scoped to the version being read. Its `@pagefind/default-ui` import + // is redirected to the subclass in src/lib/pagefind-ui.ts, which + // selects the filters of the current page. + name: 'gitea-pagefind-filters', + enforce: 'pre', + resolveId(source, importer) { + if ( + !useDocSearch && + source === '@pagefind/default-ui' && + importer?.includes('starlight/components/Search.astro') + ) { + pagefindUiRedirected = true; + return path.join(repoRoot, 'sites/docs/src/lib/pagefind-ui.ts'); + } + return null; + }, + }, ], }, // languages the sources tag code blocks with that shiki does not know @@ -62,6 +87,19 @@ export default defineConfig({ }, integrations: [ giteaPostBuild(), + { + name: 'gitea-pagefind-filters-check', + hooks: { + 'astro:build:done': () => { + if (useDocSearch || pagefindUiRedirected) return; + throw new Error( + 'the pagefind search ui was not replaced by src/lib/pagefind-ui.ts, ' + + 'the search would return results from every version: check the ' + + "`gitea-pagefind-filters` plugin against starlight's Search.astro", + ); + }, + }, + }, starlight({ title: 'Gitea Documentation', description: 'Git with a cup of tea', diff --git a/sites/docs/package.json b/sites/docs/package.json index 64ed6939..4cc29854 100644 --- a/sites/docs/package.json +++ b/sites/docs/package.json @@ -14,6 +14,7 @@ "@astrojs/starlight": "0.41.7", "@astrojs/starlight-docsearch": "0.7.0", "@gitea-docs/content-loader": "workspace:*", + "@pagefind/default-ui": "^1.5.2", "astro": "7.2.0", "sharp": "0.34.5", "starlight-openapi": "0.26.0" diff --git a/sites/docs/src/components/MarkdownContent.astro b/sites/docs/src/components/MarkdownContent.astro index 75b3f569..bc21810f 100644 --- a/sites/docs/src/components/MarkdownContent.astro +++ b/sites/docs/src/components/MarkdownContent.astro @@ -1,17 +1,25 @@ --- import Default from '@astrojs/starlight/components/MarkdownContent.astro'; import { t } from '../config/strings'; +import { searchFilters } from '../lib/search'; /** * Translated pages carry the notice the docusaurus site showed: a translation * can lag behind the english original, so point at it and at the translation * guide. Fallback pages already get starlight's own notice. + * + * The wrappers carry the pagefind filters of the page. They sit inside the + * `data-pagefind-body` element starlight puts on `
` and outside the + * `.sl-markdown-content` the default component renders, so they are picked up + * by the index without taking part in the content styles. One filter per + * element: pagefind reads the whole attribute as a single `name:value` pair. */ const route = Astro.locals.starlightRoute; const meta = route.entry.data.gitea; const translated = Boolean(meta) && meta!.locale !== 'en-us' && !route.isFallback; const editUrl = route.editUrl?.href; const strings = t(meta?.locale ?? 'en-us'); +const filters = meta ? searchFilters(meta) : undefined; --- { @@ -22,7 +30,11 @@ const strings = t(meta?.locale ?? 'en-us'); ) } - +
+
+ +
+