diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs
index a059280810c..c9bfff2c238 100644
--- a/docs/astro.config.mjs
+++ b/docs/astro.config.mjs
@@ -35,6 +35,10 @@ export default defineConfig({
trailingSlash: 'always',
integrations: [
starlight({
+ // TODO(HiDeoo) Remove the overrides and associated files.
+ components: {
+ Search: './src/components/SearchOverride.astro',
+ },
title: 'Starlight',
logo: {
light: '/src/assets/logo-light.svg',
@@ -70,6 +74,11 @@ export default defineConfig({
customCss: ['./src/assets/landing.css'],
locales,
sidebar: [
+ // TODO(HiDeoo) Remove this autogenerated group.
+ {
+ label: 'Demo',
+ autogenerate: { directory: 'demo' },
+ },
{
label: 'Start Here',
translations: {
diff --git a/docs/package.json b/docs/package.json
index c400b48f4f1..b869f354159 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -6,6 +6,10 @@
"scripts": {
"test": "playwright install --with-deps chromium && playwright test",
"dev": "astro dev",
+ "comment": "// TODO(HiDeoo) Remove these pagefind related scripts",
+ "pagefind": "pnpm run --stream /^pagefind:/",
+ "pagefind:build": "astro build && astro preview",
+ "pagefind:dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
diff --git a/docs/src/assets/landing.css b/docs/src/assets/landing.css
index 88f6c63525b..b2d1a722c2b 100644
--- a/docs/src/assets/landing.css
+++ b/docs/src/assets/landing.css
@@ -15,7 +15,7 @@
60rem 30rem;
}
-[data-has-hero] header {
+[data-has-hero] .page > header {
border-bottom: 1px solid transparent;
background-color: transparent;
-webkit-backdrop-filter: blur(16px);
diff --git a/docs/src/components/SearchOverride.astro b/docs/src/components/SearchOverride.astro
new file mode 100644
index 00000000000..ded33346cc8
--- /dev/null
+++ b/docs/src/components/SearchOverride.astro
@@ -0,0 +1,64 @@
+---
+import Default from '@astrojs/starlight/components/Search.astro';
+
+// TODO(HiDeoo) Delete this override.
+
+const { id } = Astro.locals.starlightRoute;
+
+const triggerType =
+ id === 'demo/trigger-filters' ? 'filters' : id === 'demo/trigger-search' ? 'search' : undefined;
+---
+
+{
+ triggerType ? (
+
+
+
+
+
+ ) : (
+
+
+
+ )
+}
+
+
+
+
diff --git a/docs/src/content/docs/demo/trigger-filters.md b/docs/src/content/docs/demo/trigger-filters.md
new file mode 100644
index 00000000000..af50a74188b
--- /dev/null
+++ b/docs/src/content/docs/demo/trigger-filters.md
@@ -0,0 +1,11 @@
+---
+title: Trigger Filters API
+---
+
+:::danger
+// TODO(HiDeoo) Delete this page.
+:::
+
+This page exists to test a public API for triggering filters in the Pagefind search component.
+
+When doing a search from this page, e.g. for the word `test`, search results should be filtered by default to only include results that have the filter `author` set to `Alice`.
diff --git a/docs/src/content/docs/demo/trigger-search.md b/docs/src/content/docs/demo/trigger-search.md
new file mode 100644
index 00000000000..16ef235b80c
--- /dev/null
+++ b/docs/src/content/docs/demo/trigger-search.md
@@ -0,0 +1,11 @@
+---
+title: Trigger Search API
+---
+
+:::danger
+// TODO(HiDeoo) Delete this page.
+:::
+
+This page exists to test a public API for searching with the Pagefind search component.
+
+When opening the search modal from this page, a search for the word `test` should be performed by default.
diff --git a/docs/src/content/docs/guides/i18n.mdx b/docs/src/content/docs/guides/i18n.mdx
index 6e70b47b1b6..05808156abf 100644
--- a/docs/src/content/docs/guides/i18n.mdx
+++ b/docs/src/content/docs/guides/i18n.mdx
@@ -232,24 +232,6 @@ You can provide translations for additional languages you support — or overrid
}
```
- Starlight’s search modal is powered by the [Pagefind](https://pagefind.app/) library.
- You can set translations for Pagefind’s UI in the same JSON file using `pagefind` keys:
-
- ```json
- {
- "pagefind.clear_search": "Clear",
- "pagefind.load_more": "Load more results",
- "pagefind.search_label": "Search this site",
- "pagefind.filters_label": "Filters",
- "pagefind.zero_results": "No results for [SEARCH_TERM]",
- "pagefind.many_results": "[COUNT] results for [SEARCH_TERM]",
- "pagefind.one_result": "[COUNT] result for [SEARCH_TERM]",
- "pagefind.alt_search": "No results for [SEARCH_TERM]. Showing results for [DIFFERENT_TERM] instead",
- "pagefind.search_suggestion": "No results for [SEARCH_TERM]. Try one of the following searches:",
- "pagefind.searching": "Searching for [SEARCH_TERM]..."
- }
- ```
-
### Extend translation schema
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 2e4df6cfbb2..e4951ffa534 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -43,11 +43,6 @@ export default tseslint.config(
},
},
},
- // Disabled typed linting in JavaScript files.
- {
- files: ['**/*.js', '**/*.cjs', '**/*.mjs'],
- extends: [tseslint.configs.disableTypeChecked],
- },
// Disable all formatting rules.
prettierConfig,
@@ -82,6 +77,14 @@ export default tseslint.config(
// fallbacks for some types that may not be accessible in some user environments, e.g. i18n
// keys for plugins.
'@typescript-eslint/no-redundant-type-constituents': 'off',
+ // Allow for using async event handlers without needing to use an async IIFE.
+ '@typescript-eslint/no-misused-promises': ['error', { checksVoidReturn: false }],
},
+ },
+
+ // Disabled typed linting in JavaScript files.
+ {
+ files: ['**/*.js', '**/*.cjs', '**/*.mjs'],
+ extends: [tseslint.configs.disableTypeChecked],
}
);
diff --git a/packages/starlight/components/Search.astro b/packages/starlight/components/Search.astro
index d8c80d82699..afbe150a5e1 100644
--- a/packages/starlight/components/Search.astro
+++ b/packages/starlight/components/Search.astro
@@ -1,17 +1,18 @@
---
import Icon from '../user-components/Icon.astro';
import project from 'virtual:starlight/project-context';
+import Pagefind from './pagefind/Pagefind.astro';
+import type { StarlightPagefindOptions } from './pagefind/starlight-pagefind';
-const pagefindTranslations = {
- placeholder: Astro.locals.t('search.label'),
- ...Object.fromEntries(
- Object.entries(Astro.locals.t.all())
- .filter(([key]) => key.startsWith('pagefind.'))
- .map(([key, value]) => [key.replace('pagefind.', ''), value])
- ),
-};
+const clientTranslations: StarlightPagefindOptions['translations'] = {};
+for (const [key, value] of Object.entries(Astro.locals.t.all())) {
+ if (isClientTranslation(key)) clientTranslations[key] = value as string;
+}
+function isClientTranslation(key: string): key is keyof StarlightPagefindOptions['translations'] {
+ return key.startsWith('search.pagefind.client.');
+}
-const dataAttributes: DOMStringMap = { 'data-translations': JSON.stringify(pagefindTranslations) };
+const dataAttributes: DOMStringMap = { 'data-translations': JSON.stringify(clientTranslations) };
if (project.trailingSlash === 'never') dataAttributes['data-strip-trailing-slash'] = '';
---
@@ -37,17 +38,17 @@ if (project.trailingSlash === 'never') dataAttributes['data-strip-trailing-slash
- {
+ {/* TODO(HiDeoo) Restore dev mode warning. */}
+
+
@@ -75,6 +76,7 @@ if (project.trailingSlash === 'never') dataAttributes['data-strip-trailing-slash
+
+
+
+
+
diff --git a/packages/starlight/components/pagefind/starlight-pagefind-api.ts b/packages/starlight/components/pagefind/starlight-pagefind-api.ts
new file mode 100644
index 00000000000..f2fa0522fd9
--- /dev/null
+++ b/packages/starlight/components/pagefind/starlight-pagefind-api.ts
@@ -0,0 +1,41 @@
+import type { StarlightPagefind } from './starlight-pagefind';
+import type { PagefindSearchFragment } from './starlight-pagefind-js';
+
+export type { PagefindUserConfig as StarlightPagefindOptions } from '../../schemas/pagefind';
+
+/** Starlight Pagefind API. */
+export interface StarlightPagefindApi {
+ /**
+ * Set Pagefind selected filters to use in the search results.
+ * Filter names and values supplied are case-sensitive.
+ */
+ triggerFilters: (filters: PagefindSearchFragment['filters']) => void;
+ /** Perform a search with the given query. */
+ triggerSearch: (query: string) => void;
+}
+
+/**
+ * Returns the Starlight Pagefind API, waiting for the Starlight Pagefind component to be defined
+ * if necessary.
+ */
+export async function getPagefindApi(): Promise {
+ const instance = await getOrWaitForStarlightPagefind();
+
+ return {
+ triggerFilters: (...args) => instance.triggerFilters(...args),
+ triggerSearch: (...args) => instance.triggerSearch(...args),
+ };
+}
+
+/** Starlight Pagefind instance. */
+let starlightPagefind: StarlightPagefind | null = null;
+
+/** Return a fully initialized Starlight Pagefind instance, waiting for it to be ready if necessary. */
+async function getOrWaitForStarlightPagefind(): Promise {
+ if (starlightPagefind) return starlightPagefind;
+
+ await customElements.whenDefined('starlight-pagefind');
+ starlightPagefind = document.querySelector('starlight-pagefind')!;
+
+ return starlightPagefind;
+}
diff --git a/packages/starlight/components/pagefind/starlight-pagefind-filter.ts b/packages/starlight/components/pagefind/starlight-pagefind-filter.ts
new file mode 100644
index 00000000000..e4ec0e19bb8
--- /dev/null
+++ b/packages/starlight/components/pagefind/starlight-pagefind-filter.ts
@@ -0,0 +1,88 @@
+import type { PagefindFilters } from './starlight-pagefind';
+import type { PagefindFilterCounts } from './starlight-pagefind-js';
+
+/**
+ * Component rendering a Pagefind filter with its known values.
+ *
+ * @see {@link https://github.com/Pagefind/pagefind/blob/d7d0b3a0f0eb12661cd2eb894ad02f10687a4ca0/pagefind_ui/default/svelte/filters.svelte}
+ */
+export class StarlightPagefindFilter extends HTMLElement {
+ /** Pagefind filter details. */
+ #name?: string;
+ #values: PagefindFilterCounts[string] = {};
+ #selectedValues: PagefindFilters['selected'][string] = [];
+ #defaultOpen?: boolean;
+ #onToggleOpen?: (opened: boolean) => void;
+ #showEmptyValues?: boolean;
+
+ /** Various references to elements commonly used in this component. */
+ #details = this.querySelector('details')!;
+ #summary = this.querySelector('summary')!;
+
+ /** Callback called when the component is added to the document. */
+ connectedCallback() {
+ this.#details.addEventListener('toggle', this.#onDetailsToggle);
+ }
+
+ /** Set the Pagefind filter and render it. */
+ set pagefindFilter(filter: {
+ name: string;
+ values: PagefindFilterCounts[string];
+ selectedValues: PagefindFilters['selected'][string];
+ defaultOpen?: boolean;
+ onToggleOpen: (opened: boolean) => void;
+ showEmptyValues: boolean;
+ }) {
+ this.#name = filter.name;
+ this.#values = filter.values;
+ this.#selectedValues = filter.selectedValues;
+ this.#defaultOpen = filter.defaultOpen ?? false;
+ this.#onToggleOpen = filter.onToggleOpen;
+ this.#showEmptyValues = filter.showEmptyValues;
+
+ this.#render();
+ }
+
+ /** Callback called when the details element is toggled. */
+ #onDetailsToggle = () => {
+ this.#onToggleOpen?.(this.#details.open);
+ };
+
+ /** Render a filter with all its values. */
+ #render() {
+ if (!this.#name) throw new Error('Trying to render a filter without a name.');
+
+ this.#summary.textContent =
+ this.#name.charAt(0).toLocaleUpperCase(document.documentElement.lang || 'en') +
+ this.#name.slice(1);
+
+ if (this.#defaultOpen) {
+ this.#details.setAttribute('open', '');
+ }
+
+ for (const [value, count] of Object.entries(this.#values)) {
+ if (!this.#showEmptyValues && count === 0) continue;
+
+ const container = document.createElement('div');
+
+ const input = document.createElement('input');
+ input.type = 'checkbox';
+ input.id = `${this.#name}-${value}`;
+ input.name = this.#name;
+ input.value = value;
+ input.checked = this.#selectedValues.includes(value);
+ input.dataset.slPagefindFilterName = this.#name;
+ input.dataset.slPagefindFilterValue = value;
+ container.appendChild(input);
+
+ const label = document.createElement('label');
+ label.setAttribute('for', input.id);
+ label.textContent = `${value} (${count})`;
+ container.appendChild(label);
+
+ this.#details.appendChild(container);
+ }
+ }
+}
+
+customElements.define('starlight-pagefind-filter', StarlightPagefindFilter);
diff --git a/packages/starlight/components/pagefind/starlight-pagefind-js.ts b/packages/starlight/components/pagefind/starlight-pagefind-js.ts
new file mode 100644
index 00000000000..c9640bdf3cf
--- /dev/null
+++ b/packages/starlight/components/pagefind/starlight-pagefind-js.ts
@@ -0,0 +1,240 @@
+/** Pagefind client which does not seem to have definitions available anywhere. */
+export interface Pagefind {
+ filters: () => Promise;
+ mergeIndex: (url: string, options: PagefindIndexOptions) => Promise;
+ options: (options: PagefindIndexOptions) => Promise;
+ preload(query: string, options?: PagefindOptions): Promise;
+ search: (query: string, options?: PagefindOptions) => Promise;
+}
+
+/** Pagefind client options. */
+interface PagefindOptions {
+ filters: PagefindSearchFragment['filters'];
+}
+
+/**
+ * Below are various Pagefind types that are not published.
+ * @see {@link https://github.com/Pagefind/pagefind/blob/production-docs/pagefind_web_js/types/index.d.ts}
+ */
+
+/** Global index options that can be passed to pagefind.options() */
+export type PagefindIndexOptions = {
+ /** Overrides the URL path that Pagefind uses to load its search bundle */
+ basePath?: string | undefined;
+ /** Appends the given baseURL to all search results. May be a path, or a full domain */
+ baseUrl?: string | undefined;
+ /** The maximum length of excerpts that Pagefind should generate for search results. Default to 30 */
+ excerptLength?: number;
+ /**
+ * Multiply all rankings for this index by the given weight.
+ *
+ * Only applies in multisite setups, where one site should rank higher or lower than others.
+ */
+ indexWeight?: number | undefined;
+ /**
+ * Merge this filter object into all search queries in this index.
+ *
+ * Only applies in multisite setups.
+ */
+ mergeFilter?: object | undefined;
+ /**
+ * If set, will ass the search term as a query parameter under this key, for use with Pagefind's highlighting script.
+ */
+ highlightParam?: string;
+ language?: string | undefined;
+ /**
+ * Whether an instance of Pagefind is the primary index or not (for multisite).
+ *
+ * This is set for you automatically, so it is unlikely you should set this directly.
+ */
+ primary?: boolean;
+ /**
+ * Provides the ability to fine tune Pagefind's ranking algorithm to better suit your dataset.
+ */
+ ranking?: PagefindRankingWeights;
+};
+
+type PagefindRankingWeights = {
+ /**
+ * Controls page ranking based on similarity of terms to the search query (in length).
+ * Increasing this number means pages rank higher when they contain words very close to the query,
+ * e.g. if searching for `part` then `party` will boost a page higher than one containing `partition`.
+ * Minimum value is 0.0, where `party` and `partition` would be viewed equally.
+ */
+ termSimilarity?: number;
+ /**
+ * Controls how much effect the average page length has on ranking.
+ * Maximum value is 1.0, where ranking will strongly favour pages that are shorter than the average page on the site.
+ * Minimum value is 0.0, where ranking will exclusively look at term frequency, regardless of how long a document is.
+ */
+ pageLength?: number;
+ /**
+ * Controls how quickly a term saturates on the page and reduces impact on the ranking.
+ * Maximum value is 2.0, where pages will take a long time to saturate, and pages with very high term frequencies will take over.
+ * As this number trends to 0, it does not take many terms to saturate and allow other paramaters to influence the ranking.
+ * Minimum value is 0.0, where terms will saturate immediately and results will not distinguish between one term and many.
+ */
+ termSaturation?: number;
+ /**
+ * Controls how much ranking uses term frequency versus raw term count.
+ * Maximum value is 1.0, where term frequency fully applies and is the main ranking factor.
+ * Minimum value is 0.0, where term frequency does not apply, and pages are ranked based on the raw sum of words and weights.
+ * Values between 0.0 and 1.0 will interpolate between the two ranking methods.
+ * Reducing this number is a good way to boost longer documents in your search results, as they no longer get penalized for having a low term frequency.
+ */
+ termFrequency?: number;
+};
+
+/** Filter counts returned from pagefind.filters(), and alongside results from pagefind.search() */
+export type PagefindFilterCounts = Record>;
+
+/** The main results object returned from a call to pagefind.search() */
+type PagefindSearchResults = {
+ /** All pages that match the search query and filters provided */
+ results: PagefindSearchResult[];
+ /** How many results would there have been if you had omitted the filters */
+ unfilteredResultCount: number;
+ /** Given the query and filters provided, how many remaining results are there under each filter? */
+ filters: PagefindFilterCounts;
+ /** If the searched filters were removed, how many total results for each filter are there? */
+ totalFilters: PagefindFilterCounts;
+ /** Information on how long it took Pagefind to execute this query */
+ timings: {
+ preload: number;
+ search: number;
+ total: number;
+ };
+};
+
+/** A single result from a search query, before actual data has been loaded */
+export type PagefindSearchResult = {
+ /** Pagefind's internal ID for this page, unique across the site */
+ id: string;
+ /** Pagefind's internal score for your query matching this page, that is used when ranking these results */
+ score: number;
+ /** The locations of all matching words in this page */
+ words: number[];
+ /**
+ * Calling data() loads the final data fragment needed to display this result.
+ *
+ * Only call this when you need to display the data, rather than all at once.
+ * (e.g. one page as a time, or in a scroll listener)
+ * */
+ data: () => Promise;
+};
+
+/** The useful data Pagefind provides for a search result */
+export type PagefindSearchFragment = {
+ /** Pagefind's processed URL for this page. Will include the baseUrl if configured */
+ url: string;
+ /** Pagefind's unprocessed URL for this page */
+ raw_url?: string;
+ /** The full processed content text of this page */
+ content: string;
+ /** Internal type — ignore for now */
+ raw_content?: string;
+ /** The processed excerpt for this result, with matching terms wrapping in `` elements */
+ excerpt: string;
+ /**
+ * What regions of the page matched this search query?
+ *
+ * Precalculates based on h1->6 tags with IDs, using the text between each.
+ */
+ sub_results: PagefindSubResult[];
+ /** How many total words are there on this page? */
+ word_count: number;
+ /** The locations of all matching words in this page */
+ locations: number[];
+ /**
+ * The locations of all matching words in this page,
+ * paired with data about their weight and relevance to this query
+ */
+ weighted_locations: PagefindWordLocation[];
+ /** The filter keys and values this page was tagged with */
+ filters: Record;
+ /** The metadata keys and values this page was tagged with */
+ meta: Record;
+ /**
+ * The raw anchor data that Pagefind used to generate sub_results.
+ *
+ * Contains _all_ elements that had IDs on the page, so can be used to
+ * implement your own sub result calculations with different semantics.
+ */
+ anchors: PagefindSearchAnchor[];
+};
+
+/** Data for a matched section within a page */
+export type PagefindSubResult = {
+ /**
+ * Title of this sub result — derived from the heading content.
+ *
+ * If this is a result for the section of the page before any headings with IDs,
+ * this will be the same as the page's meta.title value.
+ */
+ title: string;
+ /**
+ * Direct URL to this sub result, comprised of the page's URL plus the hash string of the heading.
+ *
+ * If this is a result for the section of the page before any headings with IDs,
+ * this will be the same as the page URL.
+ */
+ url: string;
+ /** The locations of all matching words in this segment */
+ locations: number[];
+ /**
+ * The locations of all matching words in this segment,
+ * paired with data about their weight and relevance to this query
+ */
+ weighted_locations: PagefindWordLocation[];
+ /** The processed excerpt for this segment, with matching terms wrapping in `` elements */
+ excerpt: string;
+ /**
+ * Raw data about the anchor element associated with this sub result.
+ *
+ * The omission of this field means this sub result is for text found on the page
+ * before the first heading that had an ID.
+ */
+ anchor?: PagefindSearchAnchor;
+};
+
+/** Information about a matching word on a page */
+type PagefindWordLocation = {
+ /** The weight that this word was originally tagged as */
+ weight: number;
+ /**
+ * An internal score that Pagefind calculated for this word.
+ *
+ * The absolute value is somewhat meaningless, but the value can be used
+ * in comparison to other values in this set of search results to perform custom ranking.
+ */
+ balanced_score: number;
+ /**
+ * The index of this word in the result content.
+ *
+ * Splitting the content key by whitespacing and indexing by this number
+ * will yield the correct word.
+ */
+ location: number;
+};
+
+/** Raw data about elements with IDs that Pagefind encountered when indexing the page */
+type PagefindSearchAnchor = {
+ /** What element type was this anchor? e.g. `h1`, `div` */
+ element: string;
+ /** The raw id="..." attribute contents of the element */
+ id: string;
+ /**
+ * The text content of this element.
+ *
+ * In order to prevent repeating most of the page data for every anchor,
+ * Pagefind will only take top level text nodes, or text nodes nested within
+ * inline elements such as and .
+ */
+ text?: string;
+ /**
+ * The position of this anchor in the result content.
+ * Splitting the content key by whitespacing and indexing by this number
+ * will yield the first word indexed after this element's ID was found.
+ */
+ location: number;
+};
diff --git a/packages/starlight/components/pagefind/starlight-pagefind-meta.ts b/packages/starlight/components/pagefind/starlight-pagefind-meta.ts
new file mode 100644
index 00000000000..d83ad31736e
--- /dev/null
+++ b/packages/starlight/components/pagefind/starlight-pagefind-meta.ts
@@ -0,0 +1,28 @@
+import type { StarlightPagefindSearchResult } from './starlight-pagefind';
+
+/** Component rendering metadata of a Pagefind search result. */
+export class StarlightPagefindMeta extends HTMLElement {
+ /** Search result metadata to render. */
+ #meta: StarlightPagefindSearchResult['meta'] = [];
+
+ /** Various references to elements commonly used in this component. */
+ #listitem = this.querySelector('li')!;
+
+ /** Set the Pagefind metadata and render them. */
+ set pagefindMeta(pagefindMeta: StarlightPagefindSearchResult['meta']) {
+ this.#meta = pagefindMeta;
+
+ this.#render();
+ }
+
+ /** Render the metadata of a search result. */
+ #render() {
+ for (const [key, value] of this.#meta) {
+ const div = document.createElement('div');
+ div.textContent = `${key}: ${value}`;
+ this.#listitem.appendChild(div);
+ }
+ }
+}
+
+customElements.define('starlight-pagefind-meta', StarlightPagefindMeta);
diff --git a/packages/starlight/components/pagefind/starlight-pagefind-result.ts b/packages/starlight/components/pagefind/starlight-pagefind-result.ts
new file mode 100644
index 00000000000..af7cdbd7348
--- /dev/null
+++ b/packages/starlight/components/pagefind/starlight-pagefind-result.ts
@@ -0,0 +1,60 @@
+import type { StarlightPagefindSearchResult } from './starlight-pagefind';
+
+/**
+ * Component rendering a single Pagefind search result with potential sub-results.
+ *
+ * @see {@link https://github.com/Pagefind/pagefind/blob/d7d0b3a0f0eb12661cd2eb894ad02f10687a4ca0/pagefind_ui/default/svelte/result_with_subs.svelte}
+ */
+export class StarlightPagefindResult extends HTMLElement {
+ /** Starlight Pagefind search result ready to be rendered. */
+ #searchResult?: StarlightPagefindSearchResult;
+
+ /** Various references to elements commonly used in this component. */
+ #link = this.querySelector('a')!;
+
+ /** Set the Starlight Pagefind search result and render it. */
+ set pagefindResult(pagefindResult: StarlightPagefindSearchResult) {
+ this.#searchResult = pagefindResult;
+
+ this.#render();
+ }
+
+ /** Get the Pagefind ID of a search result. */
+ get resultId() {
+ if (!this.#searchResult) {
+ throw new Error('Trying to access the pagefind ID of a result that has not been set yet.');
+ }
+
+ return this.#searchResult?.id;
+ }
+
+ /** Mark this result as selected, e.g. when navigating with the keyboard. */
+ set selected(selected: boolean) {
+ if (!selected) {
+ this.#link.removeAttribute('aria-selected');
+ return;
+ }
+
+ this.#link.setAttribute('aria-selected', 'true');
+ }
+
+ /** Render the search result. */
+ #render() {
+ if (!this.#searchResult) throw new Error('Trying to render a search result that is not set.');
+
+ if (this.#searchResult.isSubResult) this.dataset.slPagefindSubResult = 'true';
+ else this.dataset.slPagefindRootResult = 'true';
+
+ this.#link.id = this.#searchResult.id;
+ this.#link.href = this.#searchResult.href;
+
+ this.#link.querySelector('.sl-pagefind-result-title')!.textContent = this.#searchResult.title;
+
+ if (this.#searchResult.excerpt) {
+ this.#link.querySelector('.sl-pagefind-result-excerpt')!.innerHTML =
+ this.#searchResult.excerpt;
+ }
+ }
+}
+
+customElements.define('starlight-pagefind-result', StarlightPagefindResult);
diff --git a/packages/starlight/components/pagefind/starlight-pagefind.ts b/packages/starlight/components/pagefind/starlight-pagefind.ts
new file mode 100644
index 00000000000..0fdc53d9a44
--- /dev/null
+++ b/packages/starlight/components/pagefind/starlight-pagefind.ts
@@ -0,0 +1,801 @@
+import type { PagefindConfig } from '../../schemas/pagefind';
+import type {
+ Pagefind,
+ PagefindFilterCounts,
+ PagefindIndexOptions,
+ PagefindSearchFragment,
+ PagefindSearchResult,
+ PagefindSubResult,
+} from './starlight-pagefind-js';
+import type { StarlightPagefindResult } from './starlight-pagefind-result';
+import type { StarlightPagefindMeta } from './starlight-pagefind-meta';
+import type { StarlightPagefindFilter } from './starlight-pagefind-filter';
+import type { StarlightPagefindApi } from './starlight-pagefind-api';
+
+/**
+ * Various configuration options for the Starlight Pagefind component that are either not
+ * user-configurable in Starlight or hardcoded values extracted from the original Pagefind UI.
+ */
+const starlightPagefindConfig = {
+ /** Amount of milliseconds to wait before performing a search after the user stops typing. */
+ debounceTimeoutMs: 300,
+ /** Length of excerpts to show in search results. */
+ excerptLength: 12,
+ /** Number of root results to show per page. */
+ resultsPerPage: 5,
+ /**
+ * A list of meta keys to skip when processing results.
+ * @see {@link https://github.com/Pagefind/pagefind/blob/d7d0b3a0f0eb12661cd2eb894ad02f10687a4ca0/pagefind_ui/default/svelte/result_with_subs.svelte#L6}
+ */
+ skipMeta: new Set(['title', 'image', 'image_alt', 'url']),
+ /** Number of sub-results to show per root result. */
+ subResultsPerResult: 3,
+};
+
+/**
+ * A Starlight-specific rewrite of the original Pagefind UI.
+ *
+ * @see {@link https://github.com/Pagefind/pagefind/tree/main/pagefind_ui/default}
+ * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-autocomplete-list/}
+ * @see {@link https://www.makethingsaccessible.com/guides/accessible-site-search-with-combobox-suggestions/}
+ */
+export class StarlightPagefind extends HTMLElement implements StarlightPagefindApi {
+ /** Options for the Starlight Pagefind component. */
+ #options: StarlightPagefindOptions = { bundlePath: '/pagefind/' };
+ /** Pagefind options. */
+ #pagefindOptions?: PagefindOptions;
+
+ /** Various references to elements commonly used in this component. */
+ #dialog = this.closest('dialog')!;
+ #form = this.querySelector('form')!;
+ #queryInput = this.querySelector('input[type="search"]')!;
+ #status = this.querySelector('.sl-pagefind-status')!;
+ #filters = this.querySelector('.sl-pagefind-filters')!;
+ #filterTemplate = this.querySelector('#sl-pagefind-filter-tpl')!;
+ #scrollable = this.#dialog.querySelector('.sl-pagefind-content')!;
+ #listbox = this.querySelector('ul')!;
+ #metaTemplate = this.querySelector('#sl-pagefind-meta-tpl')!;
+ #resultTemplate = this.querySelector('#sl-pagefind-result-tpl')!;
+ #clearButton = this.querySelector('button.sl-pagefind-clear')!;
+ #showMoreButton = this.querySelector('button.sl-pagefind-show-more')!;
+ #selectedResult?: StarlightPagefindResult | null;
+
+ /** Pagefind instance and search results. */
+ #pagefind?: Pagefind;
+ #pagefindResults: PagefindSearchResult[] = [];
+ #pagefindQuery = '';
+ #pagefindFilters: PagefindFilters = { initial: {}, available: {}, opened: {}, selected: {} };
+ #starlightPagefindResults: StarlightPagefindSearchResult[] = [];
+
+ /** A match media query to check if getting a specific result into view should be animated or not. */
+ #prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
+
+ /** Number formatter used to format numbers in translations. */
+ #numberFormatter = new Intl.NumberFormat(document.documentElement.lang || 'en');
+
+ /**
+ * An abort controller and timeout reference used to orchestrate searches.
+ * @see {@link StarlightPagefind.#search} for more details.
+ */
+ #seachAbortController?: AbortController | undefined;
+ #searchTimeout?: ReturnType;
+
+ /**
+ * Various state variables used to track the state of the component.
+ * These states are not mutually exclusive, so multiple states can be true at the same time.
+ */
+ #didTryLoadingPagefind = false;
+ #isPerformingSearch = false;
+
+ /** Callback called when the component is added to the document. */
+ connectedCallback() {
+ this.#attachEventListeners();
+ }
+
+ /* ------------ Event listeners ----------- */
+
+ /** Attach all required event listeners. */
+ #attachEventListeners() {
+ this.#form.addEventListener('change', this.#onFormChange);
+
+ this.#queryInput.addEventListener('focus', this.#loadPagefind);
+ this.#queryInput.addEventListener('keydown', this.#onQueryInputKeyDown);
+ this.#queryInput.addEventListener('input', this.#onQueryInputChange);
+
+ this.#showMoreButton.addEventListener('click', this.#onShowMoreButtonClick);
+
+ this.#clearButton.addEventListener('click', this.#onClearButtonClick);
+ }
+
+ /** Handle form changes, specifically checkbox inputs for filters. */
+ #onFormChange = async (event: Event) => {
+ if (!(event.target instanceof HTMLInputElement) || event.target.type !== 'checkbox') return;
+
+ const filters: PagefindFilters['selected'] = {};
+
+ // Collect all checked filters and immediately transform them into the expected format.
+ // This is a different approach than the original Pagefind UI which transforms them
+ // for each search.
+ for (const filter of this.#filters.querySelectorAll(
+ 'input[type="checkbox"]:checked'
+ )) {
+ const name = filter.dataset.slPagefindFilterName;
+ const value = filter.dataset.slPagefindFilterValue;
+ if (!name || !value) continue;
+
+ filters[name] ??= [];
+ filters[name].push(value);
+ }
+
+ this.#pagefindFilters.selected = filters;
+ await this.#search();
+ };
+
+ /** Handle keydown events on the query input. */
+ #onQueryInputKeyDown = (event: KeyboardEvent) => {
+ if (!(event.target instanceof HTMLInputElement)) return;
+
+ // https://www.w3.org/WAI/ARIA/apg/patterns/combobox/#keyboardinteraction
+ switch (event.key) {
+ case 'ArrowDown': {
+ event.preventDefault();
+ this.#updateSelectedResult('next');
+ break;
+ }
+ case 'ArrowUp': {
+ event.preventDefault();
+ this.#updateSelectedResult('prev');
+ break;
+ }
+ // The Home and End keys place the editing cursor at the beginning/end of the field.
+ // The wrapping behavior of the Up and Down arrows is used as a substitute for this.
+ // https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-autocomplete-both/#kbd_label_listbox
+ case 'Enter': {
+ event.preventDefault();
+ this.#openSelection();
+ break;
+ }
+ }
+ };
+
+ /** Handle query input changes. */
+ #onQueryInputChange = async (event: Event) => {
+ if (!(event.target instanceof HTMLInputElement)) return;
+ this.#pagefindQuery = event.target.value;
+ this.#renderClearButton();
+ await this.#search();
+ };
+
+ /** Handle clicks on the button to show more results. */
+ #onShowMoreButtonClick = async (event: Event) => {
+ event.preventDefault();
+ await this.#transformPagefindResults();
+ this.#render();
+ };
+
+ /** Handle clicks on the button to clear the query input. */
+ #onClearButtonClick = () => {
+ this.#queryInput.value = '';
+ this.#queryInput.focus();
+ this.#clearPagefindResults();
+ this.#renderClearButton();
+ };
+
+ /* --------------- Pagefind --------------- */
+
+ /** Set the options for the Starlight Pagefind component and the Pagefind library. */
+ init(starlightPagefindOptions: StarlightPagefindOptions, pagefindOptions: PagefindOptions) {
+ this.#options = { ...this.#options, ...starlightPagefindOptions };
+ this.#pagefindOptions = pagefindOptions;
+ }
+
+ /** Load Pagefind, set its options, merge indexes, and load filters. */
+ #loadPagefind = async () => {
+ if (this.#didTryLoadingPagefind) return;
+ this.#didTryLoadingPagefind = true;
+ if (this.#pagefind) return;
+ if (!this.#pagefindOptions) throw new Error('Trying to load Pagefind with no options.');
+
+ // We only assign pagefind to `this.#pagefind` after potentially merging indexes.
+ let pagefind: Pagefind | undefined;
+
+ const pagefindPath = `${this.#options.bundlePath}pagefind.js`;
+
+ try {
+ pagefind = (await import(/* @vite-ignore */ pagefindPath)) as Pagefind;
+ } catch {
+ console.error(`Failed to load Pagefind from ${pagefindPath}`);
+ }
+
+ await pagefind?.options({
+ excerptLength: starlightPagefindConfig.excerptLength,
+ ...this.#pagefindOptions,
+ });
+
+ for (const index of this.#options.mergeIndex ?? []) {
+ const { bundlePath, ...indexOptions } = index;
+ await pagefind?.mergeIndex(bundlePath, indexOptions);
+ }
+
+ if (pagefind) this.#pagefind = pagefind;
+
+ await this.#loadFilters();
+ };
+
+ /** Return a fully initialized Pagefind instance, waiting for it to be ready if necessary. */
+ #getOrWaitForPagefind() {
+ if (this.#pagefind) return this.#pagefind;
+
+ return new Promise((resolve) => {
+ const interval = setInterval(() => {
+ if (this.#pagefind) {
+ clearInterval(interval);
+ resolve(this.#pagefind);
+ }
+ }, 50);
+ });
+ }
+
+ /** Load Pagefind initial filters, if any. */
+ async #loadFilters() {
+ if (!this.#pagefind) return;
+
+ this.#pagefindFilters.initial = await this.#pagefind.filters();
+
+ if (Object.keys(this.#pagefindFilters.available).length === 0) {
+ this.#pagefindFilters.available = this.#pagefindFilters.initial;
+ }
+ }
+
+ /** Start a search for a given query, debounced by a timeout. */
+ async #search() {
+ if (this.#searchTimeout) clearTimeout(this.#searchTimeout);
+
+ if (!this.#pagefindQuery) {
+ this.#clearPagefindResults();
+ return;
+ }
+
+ // Process the term before performing or preloading a search (the original Pagefind UI
+ // implementation only does this when performing a search which seems to be a bug).
+ const query = this.#options.processTerm
+ ? this.#options.processTerm(this.#pagefindQuery)
+ : this.#pagefindQuery;
+
+ this.#searchTimeout = setTimeout(async () => {
+ this.#seachAbortController?.abort();
+ this.#seachAbortController = new AbortController();
+ const signal = this.#seachAbortController.signal;
+
+ try {
+ await this.#performSearch(query, this.#pagefindFilters.selected, signal);
+ } catch (error) {
+ if (error instanceof DOMException && error.name === 'AbortError') {
+ // Skip aborted searches.
+ return;
+ }
+ // Re-throw other errors.
+ throw error;
+ }
+ }, starlightPagefindConfig.debounceTimeoutMs);
+
+ // Even if the search is debounced, preload results for the query.
+ const pagefind = await this.#getOrWaitForPagefind();
+ await pagefind.preload(query, { filters: this.#pagefindFilters.selected });
+ }
+
+ /**
+ * Perform a search for a given query and select filters, if any. Each query is attached to a
+ * signal which can be used to abort a search if a new search is started.
+ *
+ * This approach is different from the original Pagefind UI which manually increments a counter
+ * to attach an ID to each search, and then only renders results for the latest search.
+ * The approach used here allows to abort a search in progress if a new search is started rather
+ * than having all of them completing and then only rendering the latest one.
+ */
+ async #performSearch(query: string, filters: PagefindFilters['selected'], signal: AbortSignal) {
+ this.#isPerformingSearch = true;
+ this.#renderStatus();
+
+ const pagefind = await this.#getOrWaitForPagefind();
+ signal.throwIfAborted();
+
+ const { results, filters: newFilters } = await pagefind.search(query, { filters });
+ signal.throwIfAborted();
+
+ if (Object.keys(newFilters).length > 0) this.#pagefindFilters.available = newFilters;
+
+ this.#setPagefindResults(results);
+ await this.#transformPagefindResults();
+ signal.throwIfAborted();
+
+ this.#isPerformingSearch = false;
+
+ this.#render(true);
+ }
+
+ /** Reset results and render the component with no results. */
+ #clearPagefindResults() {
+ this.#setPagefindResults([]);
+ this.#render(true);
+ }
+
+ /** Set the results of a search, clearing the previous selection and results. */
+ #setPagefindResults(results: PagefindSearchResult[]) {
+ this.#selectedResult = null;
+ this.#starlightPagefindResults = [];
+ this.#pagefindResults = results;
+ }
+
+ /**
+ * Transform multiple `PagefindSearchResult` into `StarlightPagefindSearchResult` which are ready
+ * to be rendered by the Starlight Pagefind component and contain all the necessary data.
+ * Only results that have not been rendered yet and results that should be rendered on the next
+ * page are transformed.
+ *
+ * This approach is different from the original Pagefind UI which loads the data for each
+ * result in the component rendering a result when it is needed. Such approach can lead to
+ * results with data being intertwined with results still loading their data, which can be
+ * confusing, especially on slow machines or connections.
+ * The approach used here still follows the Pagefind recommendation of not loading all results at
+ * once, but instead loads a limited number of results at a time, and only when data for all of
+ * them is ready, they are rendered at once.
+ */
+ async #transformPagefindResults() {
+ this.#starlightPagefindResults.push(
+ ...(await Promise.all(
+ this.#pagefindResults
+ // Only transform results that have not been rendered yet and that should be rendered on
+ // the next page.
+ .slice(
+ this.#starlightPagefindResults.length,
+ this.#starlightPagefindResults.length + starlightPagefindConfig.resultsPerPage
+ )
+ .map(this.#transformPagefindResult)
+ ))
+ );
+ }
+
+ /**
+ * Transform a single `PagefindSearchResult` into a `StarlightPagefindSearchResult` ready to be
+ * rendered by the Starlight Pagefind component and containing all the necessary data.
+ * Sub-results are also transformed so they can be rendered as well.
+ * @see {@link StarlightPagefind.#transformPagefindResults} for more details.
+ */
+ #transformPagefindResult = async (
+ pagefindResult: PagefindSearchResult
+ ): Promise => {
+ let data = await pagefindResult.data();
+
+ if (this.#options.processResult) data = this.#options.processResult(data);
+
+ const id = pagefindResult.id;
+ const hasRootSubResult = data.sub_results.at(0)?.url === (data.meta?.url || data.url);
+
+ const result: StarlightPagefindSearchResult = {
+ id,
+ isSubResult: false,
+ href: data.meta.url || data.url,
+ meta: Object.entries(data.meta).filter(([key]) => !starlightPagefindConfig.skipMeta.has(key)),
+ subResults: this.#getPagefindSubResults(
+ id,
+ hasRootSubResult ? data.sub_results.slice(1) : data.sub_results
+ ),
+ title: data.meta.title || '',
+ };
+
+ if (hasRootSubResult) result.excerpt = data.excerpt;
+
+ return result;
+ };
+
+ /**
+ * Returns the sorted sub-results for a given result ID, limited to
+ * {@link starlightPagefindConfig.subResultsPerResult}.
+ *
+ * @see {@link https://github.com/Pagefind/pagefind/blob/d7d0b3a0f0eb12661cd2eb894ad02f10687a4ca0/pagefind_ui/default/svelte/result_with_subs.svelte#L13-L24 | original implementation}
+ */
+ #getPagefindSubResults(resultId: string, subResults: PagefindSubResult[]) {
+ return subResults
+ .toSorted((a, b) => b.locations.length - a.locations.length)
+ .slice(0, starlightPagefindConfig.subResultsPerResult)
+ .map((subResult, index) => {
+ return {
+ excerpt: subResult.excerpt,
+ href: subResult.url,
+ id: `${resultId}-${index}`,
+ isSubResult: true,
+ meta: [],
+ title: subResult.title,
+ };
+ });
+ }
+
+ /* ------ Starlight Pagefind results ------ */
+
+ /** Update the selected result based on s specified type of selection change. */
+ #updateSelectedResult(type: 'first' | 'prev' | 'next') {
+ switch (type) {
+ case 'prev':
+ case 'next': {
+ if (!this.#selectedResult) {
+ this.#selectedResult =
+ type === 'prev' ? this.#results.item(this.#results.length - 1) : this.#results.item(0);
+ break;
+ }
+
+ const currentIndex = Array.from(this.#results).findIndex(
+ (result) => result.resultId === this.#selectedResult?.resultId
+ );
+
+ let newIndex = currentIndex + (type === 'prev' ? -1 : 1);
+ if (type === 'prev' && newIndex < 0) newIndex = this.#results.length - 1;
+ else if (type === 'next' && newIndex >= this.#results.length) newIndex = 0;
+
+ this.#selectedResult = this.#results.item(newIndex);
+ break;
+ }
+ // Select the first result by default.
+ default: {
+ this.#selectedResult = this.#results.item(0);
+ break;
+ }
+ }
+
+ this.#renderSelection();
+ }
+
+ /** A collection of all rendered root results, excluding sub-results. */
+ get #rootResults() {
+ return this.#listbox.querySelectorAll(
+ 'starlight-pagefind-result[data-sl-pagefind-root-result]'
+ );
+ }
+
+ /** A collection of all rendered results, including sub-results. */
+ get #results() {
+ return this.#listbox.querySelectorAll('starlight-pagefind-result');
+ }
+
+ /**
+ * Append a result to the listbox, creating a new `starlight-pagefind-result` element.
+ * This can be a root result or a sub-result.
+ */
+ #appendResult(starlightPagefindResult: StarlightPagefindSearchResult) {
+ const [node, result] = this.#cloneComponentTemplate(
+ this.#resultTemplate,
+ 'starlight-pagefind-result'
+ );
+
+ this.#listbox.appendChild(node);
+ result.pagefindResult = starlightPagefindResult;
+ }
+
+ /**
+ * Append metadatas to the listbox, creating a new `starlight-pagefind-meta` element.
+ */
+ #appendMeta(starlightPagefindResultMeta: StarlightPagefindSearchResult['meta']) {
+ const [node, meta] = this.#cloneComponentTemplate(
+ this.#metaTemplate,
+ 'starlight-pagefind-meta'
+ );
+
+ this.#listbox.appendChild(node);
+ meta.pagefindMeta = starlightPagefindResultMeta;
+ }
+
+ /* --------------- Rendering -------------- */
+
+ /**
+ * Really basic i18n function matching i18next's interpolation syntax with support for plurals.
+ *
+ * @see {@link https://www.i18next.com/translation-function/interpolation}
+ * @see {@link https://www.i18next.com/translation-function/plurals}
+ */
+ #t(
+ key: keyof NonNullable,
+ interpolations?: Record
+ ): string {
+ let result = this.#options.translations?.[key] ?? key;
+ if (interpolations) {
+ for (const [name, value] of Object.entries(interpolations)) {
+ if (typeof value === 'number') {
+ // Only the `zero`, `one`, and `other` forms are supported which match the original
+ // Pagefind UI implementation. Based on feedback, we can add more forms later if needed.
+ const suffix = value === 1 ? 'one' : value === 0 ? 'zero' : 'other';
+ const { [name]: omittedName, ...rest } = interpolations;
+ return this.#t(`${key}_${suffix}`, {
+ ...rest,
+ [name]: this.#numberFormatter.format(value),
+ });
+ } else {
+ result = result.replace(new RegExp(`{{${name}}}`, 'g'), value);
+ }
+ }
+ }
+ return result;
+ }
+
+ /** Clone a Starlight Pagefind component template and return the cloned node and the component itself. */
+ #cloneComponentTemplate(
+ template: HTMLTemplateElement,
+ conponent: `starlight-pagefind-${string}`
+ ): [DocumentFragment, T] {
+ const node = template.content.cloneNode(true) as DocumentFragment;
+ const component = node.querySelector(conponent)!;
+ return [node, component];
+ }
+
+ /** Render the entire component, optionally clearing the listbox for new searches. */
+ #render(isNewSearch = false) {
+ if (isNewSearch) this.#listbox.textContent = '';
+
+ this.#renderStatus();
+ this.#renderFilters();
+ this.#renderResults();
+ this.#renderShowMoreButton();
+
+ // Add keyboard access to the scrollable area if it is scrollable.
+ if (this.#scrollable.scrollHeight > this.#scrollable.clientHeight) {
+ this.#scrollable.setAttribute('tabindex', '0');
+ } else {
+ this.#scrollable.removeAttribute('tabindex');
+ }
+ }
+
+ /** Render a status message based on the current state of the search. */
+ #renderStatus() {
+ if (!this.#queryInput.value) {
+ this.#status.textContent = '';
+ return;
+ } else if (this.#isPerformingSearch) {
+ this.#status.textContent = this.#t('search.pagefind.client.searching', {
+ query: this.#queryInput.value,
+ });
+ return;
+ }
+
+ this.#status.textContent = this.#t('search.pagefind.client.results', {
+ count: this.#pagefindResults.length,
+ query: this.#queryInput.value,
+ });
+ }
+
+ /** Render the filters UI based on the available filters and the selected ones. */
+ #renderFilters() {
+ const filters = Object.entries(this.#pagefindFilters.available);
+
+ if (filters.length === 0 || !this.#queryInput.value) {
+ this.#filters.classList.add('sl-hidden');
+ return;
+ }
+
+ const focusedValue = Array.from(this.#filters.querySelectorAll('input')).findIndex(
+ (input) => input === document.activeElement
+ );
+
+ const legend = this.#filters.firstElementChild!;
+ this.#filters.textContent = '';
+ this.#filters.appendChild(legend);
+
+ for (const [name, values] of filters) {
+ const [node, filter] = this.#cloneComponentTemplate(
+ this.#filterTemplate,
+ 'starlight-pagefind-filter'
+ );
+
+ this.#filters.appendChild(node);
+ filter.pagefindFilter = {
+ name,
+ values,
+ selectedValues: this.#pagefindFilters.selected[name] ?? [],
+ // Open by default if there is only one filter, if the filter was previously opened, or if
+ // it is configured to be open by default.
+ defaultOpen:
+ filters.length === 1 ||
+ (this.#pagefindFilters.opened[name] ?? false) ||
+ (this.#options.openFilters ?? []).includes(name),
+ onToggleOpen: (opened) => {
+ this.#pagefindFilters.opened[name] = opened;
+ },
+ showEmptyValues: this.#options.showEmptyFilters ?? true,
+ };
+ }
+
+ this.#filters.classList.remove('sl-hidden');
+
+ if (focusedValue !== -1) {
+ this.#filters.querySelectorAll('input')[focusedValue]?.focus();
+ }
+ }
+
+ /** Render results which are not already rendered. */
+ #renderResults() {
+ // Skip root results that have already been rendered.
+ for (let i = this.#rootResults.length; i < this.#starlightPagefindResults.length; i++) {
+ const pagefindResult = this.#starlightPagefindResults[i]!;
+ this.#renderRootResult(pagefindResult);
+ }
+
+ this.#queryInput.setAttribute(
+ 'aria-expanded',
+ String(this.#starlightPagefindResults.length > 0)
+ );
+ }
+
+ /** Render a root result, which may or may also render sub-results. */
+ #renderRootResult(starlightPagefindResult: StarlightPagefindSearchResult) {
+ this.#appendResult(starlightPagefindResult);
+
+ if (!starlightPagefindResult.subResults) return;
+
+ for (const subResult of starlightPagefindResult.subResults) {
+ this.#appendResult(subResult);
+ }
+
+ if (starlightPagefindResult.meta.length > 0) {
+ this.#appendMeta(starlightPagefindResult.meta);
+ }
+ }
+
+ /** Render the "Show More" button if there are more results to show. */
+ #renderShowMoreButton() {
+ if (this.#pagefindResults.length > this.#starlightPagefindResults.length) {
+ this.#showMoreButton.classList.remove('sl-hidden');
+ } else {
+ this.#showMoreButton.classList.add('sl-hidden');
+ }
+ }
+
+ /**
+ * Render the selection based on the currently selected result, and optionally scroll it into
+ * view.
+ */
+ #renderSelection() {
+ for (const result of this.#results) {
+ result.selected = result.resultId === this.#selectedResult?.resultId;
+ }
+
+ if (!this.#selectedResult) {
+ this.#queryInput.removeAttribute('aria-activedescendant');
+ return;
+ }
+
+ this.#queryInput.setAttribute('aria-activedescendant', this.#selectedResult.resultId);
+
+ this.#scrollSelectionIntoView();
+ }
+
+ /** Render the button to clear the query input if there is a value in it. */
+ #renderClearButton() {
+ if (this.#queryInput.value) {
+ this.#clearButton.classList.remove('sl-hidden');
+ this.#clearButton.classList.add('sl-flex');
+ } else {
+ this.#clearButton.classList.add('sl-hidden');
+ this.#clearButton.classList.remove('sl-flex');
+ }
+ }
+
+ /* --------------- Selection -------------- */
+
+ /**
+ * Open the currently selected result, if any.
+ * This is usually triggered when accepting a combobox result selection, e.g. by pressing the
+ * Enter key when the combobox input is focused.
+ * Clicking on the result itself will not trigger this method, as results are links.
+ */
+ #openSelection() {
+ if (!this.#selectedResult) return;
+ this.#selectedResult.querySelector('a')?.click();
+ }
+
+ /** Scroll the currently selected result into view if it is not already visible. */
+ #scrollSelectionIntoView() {
+ if (!this.#selectedResult) return;
+
+ const selectionRect = this.#selectedResult.getBoundingClientRect();
+ const scrollableRect = this.#scrollable.getBoundingClientRect();
+
+ const isSelectionVisible =
+ selectionRect.top >= scrollableRect.top && selectionRect.bottom <= scrollableRect.bottom;
+
+ if (isSelectionVisible) return;
+
+ let newScrollTop = this.#scrollable.scrollTop;
+ const scrollOffset = 16; // 1rem
+
+ if (selectionRect.top < scrollableRect.top) {
+ // Element above the visible area → scrolling up
+ newScrollTop += selectionRect.top - scrollableRect.top - scrollOffset;
+ } else if (selectionRect.bottom > scrollableRect.bottom) {
+ // Element below the visible area → scrolling down
+ newScrollTop += selectionRect.bottom - scrollableRect.bottom + scrollOffset;
+ }
+
+ this.#scrollable.scrollTo({
+ top: newScrollTop,
+ behavior: this.#prefersReducedMotion.matches ? 'instant' : 'smooth',
+ });
+ }
+
+ /* -------------- Public API -------------- */
+
+ /** Set Pagefind selected filters. */
+ triggerFilters(filters: PagefindSearchFragment['filters']) {
+ const selected: PagefindFilters['selected'] = {};
+
+ for (const [name, values] of Object.entries(filters)) {
+ for (const value of values) {
+ selected[name] ??= [];
+ selected[name].push(value);
+ }
+ }
+
+ this.#pagefindFilters.selected = selected;
+ }
+
+ /** Perform a search with the given query. */
+ triggerSearch(query: string) {
+ this.#queryInput.value = query;
+ this.#queryInput.dispatchEvent(new InputEvent('input'));
+ }
+}
+
+customElements.define('starlight-pagefind', StarlightPagefind);
+
+/** Starlight Pagefind options user-defined in the Starlight `pagefind` configuration. */
+type StarlightPagefindOptionsFromPagefindConfig =
+ | 'mergeIndex'
+ | 'openFilters'
+ | 'processTerm'
+ | 'showEmptyFilters';
+
+/** Options specific to the Starlight Pagefind component. */
+export type StarlightPagefindOptions = Pick<
+ PagefindConfig,
+ StarlightPagefindOptionsFromPagefindConfig
+> & {
+ /**
+ * The Pagefind bundle directory path.
+ * @see {@link https://pagefind.app/docs/ui/#bundle-path | the Pagefind documentation} for more
+ * details.
+ */
+ bundlePath?: string;
+ /**
+ * A function to process results before they are rendered.
+ * @see {@link https://pagefind.app/docs/ui/#process-result | the Pagefind documentation} for more
+ * details.
+ */
+ processResult?: (result: PagefindSearchFragment) => PagefindSearchFragment;
+ /** Translations for the Starlight Pagefind client UI. */
+ translations?: Record<`search.pagefind.client.${string}`, string>;
+};
+
+/** Pagefind specific options that can be overridden in Starlight. */
+type PagefindOptions = Partial> &
+ Pick;
+
+/**
+ * A result ready to be rendered by the Starlight Pagefind component and containing all the
+ * necessary data.
+ * Sub-results are also included so they can be rendered as well.
+ * @see {@link StarlightPagefind.#transformPagefindResults} for more details.
+ */
+export interface StarlightPagefindSearchResult {
+ excerpt?: string;
+ href: string;
+ id: string;
+ isSubResult: boolean;
+ meta: [key: string, value: string][];
+ subResults?: StarlightPagefindSearchResult[];
+ title: string;
+}
+
+/** Various Pagefind filters which can be used to filter search results. */
+export interface PagefindFilters {
+ initial: PagefindFilterCounts;
+ available: PagefindFilterCounts;
+ opened: Record;
+ selected: PagefindSearchFragment['filters'];
+}
diff --git a/packages/starlight/integrations/virtual-user-config.ts b/packages/starlight/integrations/virtual-user-config.ts
index 35be3200ea4..baa3e03c2ca 100644
--- a/packages/starlight/integrations/virtual-user-config.ts
+++ b/packages/starlight/integrations/virtual-user-config.ts
@@ -143,8 +143,11 @@ export function vitePluginStarlightUserConfig(
['', 'export const routeMiddleware = [\n'] as [string, string]
)
.join('\n') + '];',
+ 'virtual:starlight/pagefind-config':
+ typeof opts.pagefind === 'object' && 'clientOptionsModule' in opts.pagefind
+ ? `export { default as pagefindUserConfig } from ${resolveId(opts.pagefind.clientOptionsModule)};`
+ : `export const pagefindUserConfig = ${JSON.stringify(opts.pagefind || {})}`,
/** Map of modules exporting Starlight’s templating components. */
- 'virtual:starlight/pagefind-config': `export const pagefindUserConfig = ${JSON.stringify(opts.pagefind || {})}`,
...virtualComponentModules,
} satisfies Record;
diff --git a/packages/starlight/package.json b/packages/starlight/package.json
index a15e788ca57..ed0609d73e4 100644
--- a/packages/starlight/package.json
+++ b/packages/starlight/package.json
@@ -37,6 +37,7 @@
"./loaders": "./loaders.ts",
"./route-data": "./route-data.ts",
"./types": "./types.ts",
+ "./pagefind": "./components/pagefind/starlight-pagefind-api.ts",
"./expressive-code": {
"types": "./expressive-code.d.ts",
"default": "./expressive-code.mjs"
@@ -63,7 +64,6 @@
"@astrojs/markdown-remark": "^7.0.0",
"@astrojs/mdx": "^5.0.0",
"@astrojs/sitemap": "^3.7.1",
- "@pagefind/default-ui": "^1.3.0",
"@types/hast": "^3.0.4",
"@types/js-yaml": "^4.0.9",
"@types/mdast": "^4.0.4",
diff --git a/packages/starlight/schemas/i18n.ts b/packages/starlight/schemas/i18n.ts
index 34fe7d6f5c8..6c46f74471f 100644
--- a/packages/starlight/schemas/i18n.ts
+++ b/packages/starlight/schemas/i18n.ts
@@ -21,7 +21,6 @@ interface i18nSchemaOpts {
const defaultI18nSchema = () =>
z.object({
...starlightI18nSchema().shape,
- ...pagefindI18nSchema().shape,
...expressiveCodeI18nSchema().shape,
});
/** Type of Starlight’s default i18n schema, including extensions from Pagefind and Expressive Code. */
@@ -61,7 +60,6 @@ export type i18nSchemaOutput = z.output>;
export function builtinI18nSchema() {
return z.object({
...z.strictObject({ ...starlightI18nSchema().required().shape }).shape,
- ...pagefindI18nSchema().shape,
...expressiveCodeI18nSchema().shape,
});
}
@@ -89,6 +87,42 @@ function starlightI18nSchema() {
description: 'Warning displayed when opening the Search in a dev environment.',
}),
+ 'search.pagefind.clear': z
+ .string()
+ .meta({ description: 'Text for the “Clear” input button in the search modal.' }),
+
+ 'search.pagefind.filters': z
+ .string()
+ .meta({ description: 'Title for the filters section in the search modal.' }),
+
+ 'search.pagefind.loadMore': z
+ .string()
+ .meta({ description: 'Text for the “Load more results” button in the search modal.' }),
+
+ 'search.pagefind.client.searching': z
+ .string()
+ .meta({ description: 'Status text displayed in the search modal while searching.' }),
+
+ 'search.pagefind.client.results_zero': z
+ .string()
+ .meta({
+ description: 'Status text displayed in the search modal when there are no results.',
+ }),
+
+ 'search.pagefind.client.results_one': z
+ .string()
+ .meta({
+ description:
+ 'Status text displayed in the search modal when there is exactly one result.',
+ }),
+
+ 'search.pagefind.client.results_other': z
+ .string()
+ .meta({
+ description:
+ 'Status text displayed in the search modal when there are more than one result.',
+ }),
+
'themeSelect.accessibleLabel': z
.string()
.meta({ description: 'Accessible label for the theme selection dropdown.' }),
@@ -169,62 +203,6 @@ function starlightI18nSchema() {
.partial();
}
-function pagefindI18nSchema() {
- return z
- .object({
- 'pagefind.clear_search': z.string().meta({
- description:
- 'Pagefind UI translation. English default value: `"Clear"`. See https://pagefind.app/docs/ui/#translations',
- }),
-
- 'pagefind.load_more': z.string().meta({
- description:
- 'Pagefind UI translation. English default value: `"Load more results"`. See https://pagefind.app/docs/ui/#translations',
- }),
-
- 'pagefind.search_label': z.string().meta({
- description:
- 'Pagefind UI translation. English default value: `"Search this site"`. See https://pagefind.app/docs/ui/#translations',
- }),
-
- 'pagefind.filters_label': z.string().meta({
- description:
- 'Pagefind UI translation. English default value: `"Filters"`. See https://pagefind.app/docs/ui/#translations',
- }),
-
- 'pagefind.zero_results': z.string().meta({
- description:
- 'Pagefind UI translation. English default value: `"No results for [SEARCH_TERM]"`. See https://pagefind.app/docs/ui/#translations',
- }),
-
- 'pagefind.many_results': z.string().meta({
- description:
- 'Pagefind UI translation. English default value: `"[COUNT] results for [SEARCH_TERM]"`. See https://pagefind.app/docs/ui/#translations',
- }),
-
- 'pagefind.one_result': z.string().meta({
- description:
- 'Pagefind UI translation. English default value: `"[COUNT] result for [SEARCH_TERM]"`. See https://pagefind.app/docs/ui/#translations',
- }),
-
- 'pagefind.alt_search': z.string().meta({
- description:
- 'Pagefind UI translation. English default value: `"No results for [SEARCH_TERM]. Showing results for [DIFFERENT_TERM] instead"`. See https://pagefind.app/docs/ui/#translations',
- }),
-
- 'pagefind.search_suggestion': z.string().meta({
- description:
- 'Pagefind UI translation. English default value: `"No results for [SEARCH_TERM]. Try one of the following searches:"`. See https://pagefind.app/docs/ui/#translations',
- }),
-
- 'pagefind.searching': z.string().meta({
- description:
- 'Pagefind UI translation. English default value: `"Searching for [SEARCH_TERM]..."`. See https://pagefind.app/docs/ui/#translations',
- }),
- })
- .partial();
-}
-
function expressiveCodeI18nSchema() {
return z
.object({
diff --git a/packages/starlight/schemas/pagefind.ts b/packages/starlight/schemas/pagefind.ts
index 3a470b1ca98..6dc43015542 100644
--- a/packages/starlight/schemas/pagefind.ts
+++ b/packages/starlight/schemas/pagefind.ts
@@ -71,40 +71,100 @@ const pagefindIndexOptionsSchema = z.object({
ranking: pagefindRankingWeightsSchema.prefault({}),
});
-const pagefindSchema = z.object({
- /**
- * Configure how search results from the current website are weighted by Pagefind
- * compared to results from other sites when using the `mergeIndex` option.
- *
- * @see https://pagefind.app/docs/multisite/#changing-the-weighting-of-individual-indexes
- */
- indexWeight: indexWeightSchema,
- /** Configure how search result rankings are calculated by Pagefind. */
- ranking: pagefindRankingWeightsSchema.prefault({}),
- /**
- * Configure how search indexes from different sites are merged by Pagefind.
- *
- * @see https://pagefind.app/docs/multisite/#searching-additional-sites-from-pagefind-ui
- */
- mergeIndex: z
- .array(
- /**
- * Each entry of this array represents a `PagefindIndexOptions` from pagefind.
- *
- * @see https://github.com/CloudCannon/pagefind/blob/v1.3.0/pagefind_web_js/lib/coupled_search.ts#L549
- */
- z.object({
- ...pagefindIndexOptionsSchema.shape,
+const pagefindSchema = z
+ .object({
+ /**
+ * Configure how search results from the current website are weighted by Pagefind
+ * compared to results from other sites when using the `mergeIndex` option.
+ *
+ * @see https://pagefind.app/docs/multisite/#changing-the-weighting-of-individual-indexes
+ */
+ indexWeight: indexWeightSchema,
+ /** Configure how search result rankings are calculated by Pagefind. */
+ ranking: pagefindRankingWeightsSchema.prefault({}),
+ /**
+ * Configure how search indexes from different sites are merged by Pagefind.
+ *
+ * @see https://pagefind.app/docs/multisite/#searching-additional-sites-from-pagefind-ui
+ */
+ mergeIndex: z
+ .array(
/**
- * Set Pagefind’s `bundlePath` mergeIndex option.
+ * Each entry of this array represents a `PagefindIndexOptions` from pagefind.
*
- * @see https://pagefind.app/docs/multisite/#searching-additional-sites-from-pagefind-ui
+ * @see https://github.com/CloudCannon/pagefind/blob/v1.3.0/pagefind_web_js/lib/coupled_search.ts#L549
*/
- bundlePath: z.string(),
- })
- )
- .optional(),
-});
+ z.object({
+ ...pagefindIndexOptionsSchema.shape,
+ /**
+ * Set Pagefind’s `bundlePath` mergeIndex option.
+ *
+ * @see https://pagefind.app/docs/multisite/#searching-additional-sites-from-pagefind-ui
+ */
+ bundlePath: z.string(),
+ })
+ )
+ .optional(),
+ /**
+ * Define a list of filters that should be open by default when the search results are displayed.
+ * By default, a filter is open only if it's the only filter available.
+ *
+ * @default []
+ */
+ openFilters: z.string().array().optional(),
+ /**
+ * Defines a function called before performing a search that can be used to normalize the
+ * search term.
+ */
+ processTerm: z.function({ input: [z.string()], output: z.string() }).optional(),
+ /**
+ * Configure if filter values with no results should be visible or not.
+ *
+ * @default true
+ */
+ showEmptyFilters: z.boolean().optional(),
+ })
+ .strict();
+
+const pagefindModuleSchema = z
+ .object({
+ /**
+ * The path to a JavaScript or TypeScript file containing a default export of options to
+ * pass to the Pagefind client.
+ *
+ * The value can be a path to a local JS/TS file relative to the root of your project,
+ * e.g. `'./src/pagefind.js'`, or an npm module specifier for a package you installed,
+ * e.g. `'@company/pagefind-config'`.
+ *
+ * Use `clientOptionsModule` when you need to configure options that are not serializable,
+ * such as `processTerm()`.
+ *
+ * When `clientOptionsModule` is set, all options must be set via the module file. Other
+ * inline options passed to the plugin in `astro.config.mjs` will be ignored.
+ *
+ * @example
+ * // astro.config.mjs
+ * // ...
+ * pagefind: { clientOptionsModule: './src/config/pagefind.ts' }
+ * // ...
+ *
+ * // src/config/pagefind.ts
+ * import type { StarlightPagefindOptions } from '@astrojs/starlight/pagefind';
+ *
+ * export default {
+ * openFilters: ['author'],
+ * processTerm(term) {
+ * return term.replace(/aa/g, 'ā');
+ * },
+ * } satisfies StarlightPagefindOptions;
+ */
+ clientOptionsModule: z.string(),
+ })
+ .strict();
export const PagefindConfigSchema = () => pagefindSchema;
+export const PagefindModuleConfigSchema = () => pagefindModuleSchema;
export const PagefindConfigDefaults = () => pagefindSchema.parse({});
+
+export type PagefindUserConfig = z.input;
+export type PagefindConfig = z.output;
diff --git a/packages/starlight/translations/ar.json b/packages/starlight/translations/ar.json
index 5d02881748b..4dfc9658def 100644
--- a/packages/starlight/translations/ar.json
+++ b/packages/starlight/translations/ar.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "إلغاء",
"search.devWarning": "البحث متوفر فقط في بنيات اﻹنتاج. \n جرب بناء المشروع ومعاينته على جهازك",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "اختر سمة",
"themeSelect.dark": "داكن",
"themeSelect.light": "فاتح",
diff --git a/packages/starlight/translations/ca.json b/packages/starlight/translations/ca.json
index ebd3f2dae10..0e5fa0d7abd 100644
--- a/packages/starlight/translations/ca.json
+++ b/packages/starlight/translations/ca.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Cancel·lar",
"search.devWarning": "La cerca només està disponible a les versions de producció. \nProva de construir i previsualitzar el lloc per provar-ho localment.",
+ "search.pagefind.clear": "Netejar",
+ "search.pagefind.filters": "Filtres",
+ "search.pagefind.loadMore": "Carregar més resultats",
+ "search.pagefind.client.searching": "Cercant {{query}}…",
+ "search.pagefind.client.results_zero": "Cap resultat per a: {{query}}",
+ "search.pagefind.client.results_one": "{{count}} resultat per a: {{query}}",
+ "search.pagefind.client.results_other": "{{count}} resultats per a: {{query}}",
"themeSelect.accessibleLabel": "Seleccionar tema",
"themeSelect.dark": "Fosc",
"themeSelect.light": "Clar",
@@ -29,15 +36,5 @@
"expressiveCode.terminalWindowFallbackTitle": "Finestra del terminal",
"fileTree.directory": "Directori",
"builtWithStarlight.label": "Fet amb Starlight",
- "pagefind.clear_search": "Netejar",
- "pagefind.load_more": "Carregar més resultats",
- "pagefind.search_label": "Cercar pàgina",
- "pagefind.filters_label": "Filtres",
- "pagefind.zero_results": "Cap resultat per a: [SEARCH_TERM]",
- "pagefind.many_results": "[COUNT] resultats per a: [SEARCH_TERM]",
- "pagefind.one_result": "[COUNT] resultat per a: [SEARCH_TERM]",
- "pagefind.alt_search": "Cap resultat per a [SEARCH_TERM]. Mostrant resultats per a: [DIFFERENT_TERM]",
- "pagefind.search_suggestion": "Cap resultat per a [SEARCH_TERM]. Prova alguna d’aquestes cerques:",
- "pagefind.searching": "Cercant [SEARCH_TERM]...",
"heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/translations/cs.json b/packages/starlight/translations/cs.json
index 8b2e321051e..69337ecc2f9 100644
--- a/packages/starlight/translations/cs.json
+++ b/packages/starlight/translations/cs.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Zrušit",
"search.devWarning": "Vyhledávání je dostupné pouze v produkčních sestaveních. \nZkuste sestavit a zobrazit náhled webu a otestovat jej lokálně.",
+ "search.pagefind.clear": "Vyčistit",
+ "search.pagefind.filters": "Filtry",
+ "search.pagefind.loadMore": "Načíst další výsledky",
+ "search.pagefind.client.searching": "Hledám {{query}}…",
+ "search.pagefind.client.results_zero": "Žádný výsledek pro: {{query}}",
+ "search.pagefind.client.results_one": "{{count}} výsledek pro: {{query}}",
+ "search.pagefind.client.results_other": "Počet výsledků: {{count}} pro: {{query}}",
"themeSelect.accessibleLabel": "Vyberte motiv",
"themeSelect.dark": "Tmavý",
"themeSelect.light": "Světlý",
@@ -29,15 +36,5 @@
"expressiveCode.copyButtonCopied": "Zkopírováno!",
"expressiveCode.copyButtonTooltip": "Kopíruj do schránky",
"expressiveCode.terminalWindowFallbackTitle": "Terminál",
- "pagefind.clear_search": "Vyčistit",
- "pagefind.load_more": "Načíst další výsledky",
- "pagefind.search_label": "Vyhledat stránku",
- "pagefind.filters_label": "Filtry",
- "pagefind.zero_results": "Žádný výsledek pro: [SEARCH_TERM]",
- "pagefind.many_results": "počet výsledků: [COUNT] pro: [SEARCH_TERM]",
- "pagefind.one_result": "[COUNT] výsledek pro: [SEARCH_TERM]",
- "pagefind.alt_search": "Žádné výsledky pro [SEARCH_TERM]. Namísto toho zobrazuji výsledky pro: [DIFFERENT_TERM]",
- "pagefind.search_suggestion": "Žádný výsledek pro [SEARCH_TERM]. Zkus nějaké z těchto hledání:",
- "pagefind.searching": "Hledám [SEARCH_TERM]...",
"heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/translations/da.json b/packages/starlight/translations/da.json
index 307a5f4fdb5..6f373e5c01e 100644
--- a/packages/starlight/translations/da.json
+++ b/packages/starlight/translations/da.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Annuller",
"search.devWarning": "Søgning er kun tilgængeligt i produktions versioner. \nPrøv at bygge siden og forhåndsvis den for at teste det lokalt.",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "Vælg tema",
"themeSelect.dark": "Mørk",
"themeSelect.light": "Lys",
diff --git a/packages/starlight/translations/de.json b/packages/starlight/translations/de.json
index c9cf8856158..cd45929f30d 100644
--- a/packages/starlight/translations/de.json
+++ b/packages/starlight/translations/de.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Strg",
"search.cancelLabel": "Abbrechen",
"search.devWarning": "Die Suche ist nur in Produktions-Builds verfügbar. \nVersuche, die Website zu bauen und in der Vorschau anzusehen, um sie lokal zu testen.",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "Farbschema wählen",
"themeSelect.dark": "Dunkel",
"themeSelect.light": "Hell",
diff --git a/packages/starlight/translations/el.json b/packages/starlight/translations/el.json
index 930e8647740..b36440515f4 100644
--- a/packages/starlight/translations/el.json
+++ b/packages/starlight/translations/el.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Ακύρωση",
"search.devWarning": "Η αναζήτηση είναι διαθέσιμη μόνο σε builds παραγωγής.\nΔοκίμασε να κάνεις build τον ιστότοπο και να τον προεπισκοπήσεις για να τον ελέγξεις τοπικά.",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "Επιλογή χρωματικού θέματος",
"themeSelect.dark": "Σκοτεινό",
"themeSelect.light": "Φωτεινό",
diff --git a/packages/starlight/translations/en.json b/packages/starlight/translations/en.json
index 5e968f8ce3c..890ad07afd2 100644
--- a/packages/starlight/translations/en.json
+++ b/packages/starlight/translations/en.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Cancel",
"search.devWarning": "Search is only available in production builds. \nTry building and previewing the site to test it out locally.",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "Select theme",
"themeSelect.dark": "Dark",
"themeSelect.light": "Light",
diff --git a/packages/starlight/translations/es.json b/packages/starlight/translations/es.json
index e2653ca5b9f..fbf45da3631 100644
--- a/packages/starlight/translations/es.json
+++ b/packages/starlight/translations/es.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Cancelar",
"search.devWarning": "La búsqueda solo está disponible en las versiones de producción. \nIntenta construir y previsualizar el sitio para probarlo localmente.",
+ "search.pagefind.clear": "Limpiar",
+ "search.pagefind.filters": "Filtros",
+ "search.pagefind.loadMore": "Cargar más resultados",
+ "search.pagefind.client.searching": "Buscando {{query}}…",
+ "search.pagefind.client.results_zero": "Ningún resultado para: {{query}}",
+ "search.pagefind.client.results_one": "{{count}} resultado para: {{query}}",
+ "search.pagefind.client.results_other": "{{count}} resultados para: {{query}}",
"themeSelect.accessibleLabel": "Seleccionar tema",
"themeSelect.dark": "Oscuro",
"themeSelect.light": "Claro",
@@ -29,15 +36,5 @@
"expressiveCode.terminalWindowFallbackTitle": "Ventana de terminal",
"fileTree.directory": "Directorio",
"builtWithStarlight.label": "Hecho con Starlight",
- "pagefind.clear_search": "Limpiar",
- "pagefind.load_more": "Cargar más resultados",
- "pagefind.search_label": "Buscar página",
- "pagefind.filters_label": "Filtros",
- "pagefind.zero_results": "Ningún resultado para: [SEARCH_TERM]",
- "pagefind.many_results": "[COUNT] resultados para: [SEARCH_TERM]",
- "pagefind.one_result": "[COUNT] resultado para: [SEARCH_TERM]",
- "pagefind.alt_search": "Ningún resultado para [SEARCH_TERM]. Mostrando resultados para: [DIFFERENT_TERM]",
- "pagefind.search_suggestion": "Ningún resultado para [SEARCH_TERM]. Prueba alguna de estas búsquedas:",
- "pagefind.searching": "Buscando [SEARCH_TERM]...",
"heading.anchorLabel": "Sección titulada «{{title}}»"
}
diff --git a/packages/starlight/translations/fa.json b/packages/starlight/translations/fa.json
index 22db252ddf4..4fce655e55a 100644
--- a/packages/starlight/translations/fa.json
+++ b/packages/starlight/translations/fa.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "لغو",
"search.devWarning": "جستجو تنها در نسخههای تولیدی در دسترس است. \nسعی کنید سایت را بسازید و پیشنمایش آن را به صورت محلی آزمایش کنید.",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "انتخاب پوسته",
"themeSelect.dark": "تیره",
"themeSelect.light": "روشن",
diff --git a/packages/starlight/translations/fi.json b/packages/starlight/translations/fi.json
index 520ff408eb8..da0de4de61a 100644
--- a/packages/starlight/translations/fi.json
+++ b/packages/starlight/translations/fi.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Peruuta",
"search.devWarning": "Haku on käytettävissä vain tuotantoversioissa.\nKokeile kääntää ja esikatsella sivustoa paikallisesti testataksesi sitä.",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "Valitse teema",
"themeSelect.dark": "Tumma",
"themeSelect.light": "Vaalea",
diff --git a/packages/starlight/translations/fr.json b/packages/starlight/translations/fr.json
index 25e6cea8108..327f677ab4d 100644
--- a/packages/starlight/translations/fr.json
+++ b/packages/starlight/translations/fr.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Annuler",
"search.devWarning": "La recherche est disponible uniquement en mode production. \nEssayez de construire puis de prévisualiser votre site pour tester la recherche localement.",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "Selectionner le thème",
"themeSelect.dark": "Sombre",
"themeSelect.light": "Clair",
diff --git a/packages/starlight/translations/gl.json b/packages/starlight/translations/gl.json
index 71656223045..7b17770eed8 100644
--- a/packages/starlight/translations/gl.json
+++ b/packages/starlight/translations/gl.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Deixar",
"search.devWarning": "A busca só está dispoñible nas versións de producción. \nTrata de construir e ollear o sitio para probalo localmente.",
+ "search.pagefind.clear": "Limpar",
+ "search.pagefind.filters": "Filtros",
+ "search.pagefind.loadMore": "Cargar máis resultados",
+ "search.pagefind.client.searching": "Buscando {{query}}…",
+ "search.pagefind.client.results_zero": "Ningún resultado para: {{query}}",
+ "search.pagefind.client.results_one": "{{count}} resultado para: {{query}}",
+ "search.pagefind.client.results_other": "{{count}} resultados para: {{query}}",
"themeSelect.accessibleLabel": "Selecciona tema",
"themeSelect.dark": "Escuro",
"themeSelect.light": "Claro",
@@ -29,15 +36,5 @@
"expressiveCode.terminalWindowFallbackTitle": "Fiestra do terminal",
"fileTree.directory": "Directorio",
"builtWithStarlight.label": "Feito con Starlight",
- "pagefind.clear_search": "Limpar",
- "pagefind.load_more": "Cargar máis resultados",
- "pagefind.search_label": "Buscar páxina",
- "pagefind.filters_label": "Filtros",
- "pagefind.zero_results": "Ningún resultado para: [SEARCH_TERM]",
- "pagefind.many_results": "[COUNT] resultados para: [SEARCH_TERM]",
- "pagefind.one_result": "[COUNT] resultado para: [SEARCH_TERM]",
- "pagefind.alt_search": "Ningún resultado para [SEARCH_TERM]. Amósanse resultados para: [DIFFERENT_TERM]",
- "pagefind.search_suggestion": "Ningún resultado para [SEARCH_TERM]. Proba algunha destas buscas:",
- "pagefind.searching": "Buscando [SEARCH_TERM]...",
"heading.anchorLabel": "Sección titulada «{{title}}»"
}
diff --git a/packages/starlight/translations/he.json b/packages/starlight/translations/he.json
index 0728103410d..54fe92f2492 100644
--- a/packages/starlight/translations/he.json
+++ b/packages/starlight/translations/he.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "ביטול",
"search.devWarning": "החיפוש זמין רק בסביבת ייצור. \nנסו לבנות ולהציג תצוגה מקדימה של האתר כדי לבדוק אותו באופן מקומי.",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "בחרו פרופיל צבע",
"themeSelect.dark": "כהה",
"themeSelect.light": "בהיר",
diff --git a/packages/starlight/translations/hi.json b/packages/starlight/translations/hi.json
index 6cd11a0ca76..ede8ab1a6cc 100644
--- a/packages/starlight/translations/hi.json
+++ b/packages/starlight/translations/hi.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "रद्द करे",
"search.devWarning": "खोज केवल उत्पादन बिल्ड में उपलब्ध है। \nस्थानीय स्तर पर परीक्षण करने के लिए साइट बनाए और उसका पूर्वावलोकन करने का प्रयास करें।",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "थीम चुनें",
"themeSelect.dark": "अँधेरा",
"themeSelect.light": "रोशनी",
diff --git a/packages/starlight/translations/hu.json b/packages/starlight/translations/hu.json
index d94fb7b1430..a96fadd3d85 100644
--- a/packages/starlight/translations/hu.json
+++ b/packages/starlight/translations/hu.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Mégsem",
"search.devWarning": "A keresés csak a production build-ekben működik. \nPróbáld meg először buildelni, hogy kipróbálhasd.",
+ "search.pagefind.clear": "Törlés",
+ "search.pagefind.filters": "Szűrők",
+ "search.pagefind.loadMore": "Több találat betöltése",
+ "search.pagefind.client.searching": "Keresés erre: {{query}}…",
+ "search.pagefind.client.results_zero": "Erre a kifejezésre nincs találat: {{query}}",
+ "search.pagefind.client.results_one": "{{count}} találat erre: {{query}}",
+ "search.pagefind.client.results_other": "{{count}} találat erre: {{query}}",
"themeSelect.accessibleLabel": "Téma választás",
"themeSelect.dark": "Sötét",
"themeSelect.light": "Világos",
@@ -29,15 +36,5 @@
"heading.anchorLabel": "Szekció neve “{{title}}”",
"expressiveCode.copyButtonCopied": "Másolva!",
"expressiveCode.copyButtonTooltip": "Másolás",
- "expressiveCode.terminalWindowFallbackTitle": "Terminál",
- "pagefind.clear_search": "Törlés",
- "pagefind.load_more": "Több találat betöltése",
- "pagefind.search_label": "Keresés ezen az oldalon",
- "pagefind.filters_label": "Szűrők",
- "pagefind.zero_results": "Erre a kifejezésre nincs találat: [SEARCH_TERM]",
- "pagefind.many_results": "[COUNT] találat erre: [SEARCH_TERM]",
- "pagefind.one_result": "[COUNT] találat erre: [SEARCH_TERM]",
- "pagefind.alt_search": "Erre a kifejezésre nincs találat: [SEARCH_TERM]. Találatok mutatása erre: [DIFFERENT_TERM]",
- "pagefind.search_suggestion": "Erre a kifejezésre nincs találat: [SEARCH_TERM]. Próbáld meg ezek közül az egyiket:",
- "pagefind.searching": "Keresés erre: [SEARCH_TERM]..."
+ "expressiveCode.terminalWindowFallbackTitle": "Terminál"
}
diff --git a/packages/starlight/translations/id.json b/packages/starlight/translations/id.json
index b42d3f3b7ad..36043e79937 100644
--- a/packages/starlight/translations/id.json
+++ b/packages/starlight/translations/id.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Batal",
"search.devWarning": "Pencarian hanya tersedia pada build produksi. \nLakukan proses build dan pratinjau situs Anda sebelum mencoba di lingkungan lokal.",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "Pilih tema",
"themeSelect.dark": "Gelap",
"themeSelect.light": "Terang",
diff --git a/packages/starlight/translations/it.json b/packages/starlight/translations/it.json
index 65c32f15b6f..c3c31b29e28 100644
--- a/packages/starlight/translations/it.json
+++ b/packages/starlight/translations/it.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Cancella",
"search.devWarning": "La ricerca è disponibile solo nelle build di produzione. \nProvare ad eseguire il processo di build e visualizzare la preview del sito per testarlo localmente.",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "Seleziona tema",
"themeSelect.dark": "Scuro",
"themeSelect.light": "Chiaro",
diff --git a/packages/starlight/translations/ja.json b/packages/starlight/translations/ja.json
index 2e1fd806968..209adda27ba 100644
--- a/packages/starlight/translations/ja.json
+++ b/packages/starlight/translations/ja.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "キャンセル",
"search.devWarning": "検索はプロダクションビルドでのみ利用可能です。\nローカルでテストするには、サイトをビルドしてプレビューしてください。",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "テーマの選択",
"themeSelect.dark": "ダーク",
"themeSelect.light": "ライト",
diff --git a/packages/starlight/translations/ko.json b/packages/starlight/translations/ko.json
index c8db5972fec..a066f7cfe76 100644
--- a/packages/starlight/translations/ko.json
+++ b/packages/starlight/translations/ko.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "취소",
"search.devWarning": "검색 기능은 프로덕션 빌드에서만 작동합니다. \n로컬에서 테스트하려면 사이트를 빌드하고 미리 보기를 실행하세요.",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "테마 선택",
"themeSelect.dark": "어두운 테마",
"themeSelect.light": "밝은 테마",
diff --git a/packages/starlight/translations/lv.json b/packages/starlight/translations/lv.json
index 43190fc987b..23a2164443b 100644
--- a/packages/starlight/translations/lv.json
+++ b/packages/starlight/translations/lv.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Atcelt",
"search.devWarning": "Meklēšana ir pieejama tikai ražošanas kompilācijās. \nMēģiniet kompilēt un priekšskatīt vietni, lai to pārbaudītu lokāli.",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "Izvēlieties tēmu",
"themeSelect.dark": "Tumša",
"themeSelect.light": "Gaiša",
diff --git a/packages/starlight/translations/nb.json b/packages/starlight/translations/nb.json
index 892fc435e2d..31aac438f6e 100644
--- a/packages/starlight/translations/nb.json
+++ b/packages/starlight/translations/nb.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Avbryt",
"search.devWarning": "Søk er bare tilgjengelig i produksjonsbygg. \nPrøv å bygg siden og forhåndsvis den for å teste det lokalt.",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "Velg tema",
"themeSelect.dark": "Mørk",
"themeSelect.light": "Lys",
diff --git a/packages/starlight/translations/nl.json b/packages/starlight/translations/nl.json
index e2a6cbe7c4f..bf6f4ac40c3 100644
--- a/packages/starlight/translations/nl.json
+++ b/packages/starlight/translations/nl.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Annuleren",
"search.devWarning": "Zoeken is alleen beschikbaar tijdens productie. \nProbeer om de site te builden en er een preview van te bekijken om lokaal te testen.",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "Selecteer thema",
"themeSelect.dark": "Donker",
"themeSelect.light": "Licht",
diff --git a/packages/starlight/translations/pl.json b/packages/starlight/translations/pl.json
index 8a0c475930d..e2e223016a6 100644
--- a/packages/starlight/translations/pl.json
+++ b/packages/starlight/translations/pl.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Anuluj",
"search.devWarning": "Wyszukiwanie jest dostępne tylko w buildach produkcyjnych. \nSpróbuj zbudować i uruchomić aplikację, aby przetestować lokalnie.",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "Wybierz motyw",
"themeSelect.dark": "Ciemny",
"themeSelect.light": "Jasny",
diff --git a/packages/starlight/translations/pt.json b/packages/starlight/translations/pt.json
index 25b57b2fd5b..f28dd5619ce 100644
--- a/packages/starlight/translations/pt.json
+++ b/packages/starlight/translations/pt.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Cancelar",
"search.devWarning": "A pesquisa está disponível apenas em builds em produção. \nTente fazer a build e pré-visualize o site para testar localmente.",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "Selecionar tema",
"themeSelect.dark": "Escuro",
"themeSelect.light": "Claro",
diff --git a/packages/starlight/translations/ro.json b/packages/starlight/translations/ro.json
index e7083649c86..07986a424de 100644
--- a/packages/starlight/translations/ro.json
+++ b/packages/starlight/translations/ro.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Anulează",
"search.devWarning": "Căutarea este disponibilă numai în versiunea de producție. \nÎncercă să construiești și să previzualizezi site-ul pentru a-l testa local.",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "Selectează tema",
"themeSelect.dark": "Întunecată",
"themeSelect.light": "Deschisă",
diff --git a/packages/starlight/translations/ru.json b/packages/starlight/translations/ru.json
index 1d97ae79e44..fc956dd6a64 100644
--- a/packages/starlight/translations/ru.json
+++ b/packages/starlight/translations/ru.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Отменить",
"search.devWarning": "Поиск доступен только в продакшен-сборках. \nВыполните сборку и запустите превью, чтобы протестировать поиск локально.",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "Выберите тему",
"themeSelect.dark": "Тёмная",
"themeSelect.light": "Светлая",
diff --git a/packages/starlight/translations/sk.json b/packages/starlight/translations/sk.json
index a54c83fb9c3..cd3793f137c 100644
--- a/packages/starlight/translations/sk.json
+++ b/packages/starlight/translations/sk.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Zrušiť",
"search.devWarning": "Vyhľadávanie je dostupné len v produkčných zostaveniach. \nSkúste vytvoriť a zobraziť náhľad stránky lokálne.",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "Vyberte tému",
"themeSelect.dark": "Tmavý",
"themeSelect.light": "Svetlý",
diff --git a/packages/starlight/translations/sv.json b/packages/starlight/translations/sv.json
index a834ea96090..4feb20c9c1e 100644
--- a/packages/starlight/translations/sv.json
+++ b/packages/starlight/translations/sv.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Avbryt",
"search.devWarning": "Sökfunktionen är endast tillgänglig i produktionsbyggen. \nProva att bygga och förhandsvisa siten för att testa det lokalt.",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "Välj tema",
"themeSelect.dark": "Mörkt",
"themeSelect.light": "Ljust",
diff --git a/packages/starlight/translations/th.json b/packages/starlight/translations/th.json
index ce2a780658c..155cccba3c9 100644
--- a/packages/starlight/translations/th.json
+++ b/packages/starlight/translations/th.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "ยกเลิก",
"search.devWarning": "การค้นหาสามารถใช้งานได้ในเฉพาะเวอร์ชันใช้งานจริงเท่านั้น\nโปรดลองบิลด์และดูตัวอย่างเว็บไซต์เพื่อทดสอบฟังก์ชันบนอุปกรณ์ของคุณ",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "เลือกธีม",
"themeSelect.dark": "มืด",
"themeSelect.light": "สว่าง",
diff --git a/packages/starlight/translations/tr.json b/packages/starlight/translations/tr.json
index 9ea485734ec..66104a77b74 100644
--- a/packages/starlight/translations/tr.json
+++ b/packages/starlight/translations/tr.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "İptal",
"search.devWarning": "Arama yalnızca üretim derlemelerinde kullanılabilir. \nYerel bilgisayarınızda test etmek için siteyi derleyin ve önizleme yapın.",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "Tema seç",
"themeSelect.dark": "Koyu",
"themeSelect.light": "Açık",
diff --git a/packages/starlight/translations/uk.json b/packages/starlight/translations/uk.json
index 5bcbd924a8b..0d967cdc578 100644
--- a/packages/starlight/translations/uk.json
+++ b/packages/starlight/translations/uk.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Скасувати",
"search.devWarning": "Пошук доступний лише у виробничих збірках. \nСпробуйте зібрати та переглянути сайт, щоби протестувати його локально",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "Обрати тему",
"themeSelect.dark": "Темна",
"themeSelect.light": "Світла",
diff --git a/packages/starlight/translations/vi.json b/packages/starlight/translations/vi.json
index 44e3f10efa7..3b385e3b334 100644
--- a/packages/starlight/translations/vi.json
+++ b/packages/starlight/translations/vi.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Hủy",
"search.devWarning": "Chức năng tìm kiếm chỉ có sẵn trong các phiên bản thật.\nHãy thử xây dựng và xem trước trang web để kiểm tra.",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "Chọn giao diện",
"themeSelect.dark": "Tối",
"themeSelect.light": "Sáng",
diff --git a/packages/starlight/translations/zh-CN.json b/packages/starlight/translations/zh-CN.json
index 17c0f4e98f3..4eb55e923a0 100644
--- a/packages/starlight/translations/zh-CN.json
+++ b/packages/starlight/translations/zh-CN.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "取消",
"search.devWarning": "搜索仅适用于生产版本。\n尝试构建并预览网站以在本地测试。",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "选择主题",
"themeSelect.dark": "深色",
"themeSelect.light": "浅色",
diff --git a/packages/starlight/translations/zh-TW.json b/packages/starlight/translations/zh-TW.json
index 001e1d77fe1..0527622d7a7 100644
--- a/packages/starlight/translations/zh-TW.json
+++ b/packages/starlight/translations/zh-TW.json
@@ -4,6 +4,13 @@
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "取消",
"search.devWarning": "正式版本才能使用搜尋功能。\n如要在本地測試,請先建置並預覽網站。",
+ "search.pagefind.clear": "Clear",
+ "search.pagefind.filters": "Filters",
+ "search.pagefind.loadMore": "Load more results",
+ "search.pagefind.client.searching": "Searching for {{query}}…",
+ "search.pagefind.client.results_zero": "No results for {{query}}",
+ "search.pagefind.client.results_one": "{{count}} result for {{query}}",
+ "search.pagefind.client.results_other": "{{count}} results for {{query}}",
"themeSelect.accessibleLabel": "選擇佈景主題",
"themeSelect.dark": "深色",
"themeSelect.light": "淺色",
diff --git a/packages/starlight/utils/user-config.ts b/packages/starlight/utils/user-config.ts
index c4c9b64b313..b8f2350a001 100644
--- a/packages/starlight/utils/user-config.ts
+++ b/packages/starlight/utils/user-config.ts
@@ -5,7 +5,11 @@ import { ExpressiveCodeSchema } from '../schemas/expressiveCode';
import { FaviconSchema } from '../schemas/favicon';
import { HeadConfigSchema } from '../schemas/head';
import { LogoConfigSchema } from '../schemas/logo';
-import { PagefindConfigDefaults, PagefindConfigSchema } from '../schemas/pagefind';
+import {
+ PagefindConfigDefaults,
+ PagefindConfigSchema,
+ PagefindModuleConfigSchema,
+} from '../schemas/pagefind';
import { SidebarItemSchema } from '../schemas/sidebar';
import { TitleConfigSchema, TitleTransformConfigSchema } from '../schemas/site-title';
import { SocialLinksSchema } from '../schemas/social';
@@ -199,6 +203,7 @@ const UserConfigSchema = z.object({
// Transform `true` to our default config object.
.transform((val) => val && PagefindConfigDefaults())
.or(PagefindConfigSchema())
+ .or(PagefindModuleConfigSchema())
.optional(),
/** Specify paths to components that should override Starlight’s default components */
diff --git a/packages/starlight/virtual-internal.d.ts b/packages/starlight/virtual-internal.d.ts
index 3002b4519e4..53b33184f1d 100644
--- a/packages/starlight/virtual-internal.d.ts
+++ b/packages/starlight/virtual-internal.d.ts
@@ -24,7 +24,7 @@ declare module 'virtual:starlight/route-middleware' {
declare module 'virtual:starlight/pagefind-config' {
export const pagefindUserConfig: Partial<
- Extract
+ Extract
>;
}
diff --git a/packages/starlight/vitest.config.ts b/packages/starlight/vitest.config.ts
index e9e29379335..3ee896ae8d5 100644
--- a/packages/starlight/vitest.config.ts
+++ b/packages/starlight/vitest.config.ts
@@ -31,6 +31,8 @@ export default defineConfig({
*/
// Main integration entrypoint — don’t think we’re able to test this directly currently.
// 'index.ts',
+ // Starlight Pagefind web components.
+ 'components/pagefind/**',
],
thresholds: {
lines: 87,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7f675fac8b0..0b2c09b39e5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -188,9 +188,6 @@ importers:
'@astrojs/sitemap':
specifier: ^3.7.1
version: 3.7.1
- '@pagefind/default-ui':
- specifier: ^1.3.0
- version: 1.3.0
'@types/hast':
specifier: ^3.0.4
version: 3.0.4
@@ -1188,9 +1185,6 @@ packages:
cpu: [x64]
os: [darwin]
- '@pagefind/default-ui@1.3.0':
- resolution: {integrity: sha512-CGKT9ccd3+oRK6STXGgfH+m0DbOKayX6QGlq38TfE1ZfUcPc5+ulTuzDbZUnMo+bubsEOIypm4Pl2iEyzZ1cNg==}
-
'@pagefind/linux-arm64@1.3.0':
resolution: {integrity: sha512-8lsxNAiBRUk72JvetSBXs4WRpYrQrVJXjlRRnOL6UCdBN9Nlsz0t7hWstRk36+JqHpGWOKYiuHLzGYqYAqoOnQ==}
cpu: [arm64]
@@ -5029,8 +5023,6 @@ snapshots:
'@pagefind/darwin-x64@1.3.0':
optional: true
- '@pagefind/default-ui@1.3.0': {}
-
'@pagefind/linux-arm64@1.3.0':
optional: true