Search delivers Pagefind-powered static search across all content pages. Three search surfaces are provided: a global search modal, a dedicated search page, and Pagefind integration in the docs index.
Who this is for
- Implementors configuring search behavior or adjusting which content is indexed
- Developers extending search surfaces or modifying the Pagefind integration
Architecture
Pagefind runs as a post-build step (astro build && npx pagefind --site dist), indexing all elements marked with data-pagefind-body in PageGridLayout.astro. The JS search runtime (~30KB) is lazy-loaded on first use via search-client.ts.
Key files
| File | Ownership | Purpose |
|---|---|---|
src/lib/search-client.ts | CORE | Pagefind singleton init (lazy-load + cache) and typed query wrapper. |
src/lib/search-events.ts | CORE | 5 analytics event stubs (trackSearchOpened, trackSearchSubmitted, trackSearchResultsViewed, trackSearchResultClicked, trackSearchNoResults). |
src/core/components/search/SearchModal.astro | CORE | Global modal dialog using native <dialog>, focus trap, scroll lock, Cmd/Ctrl+K shortcut. |
src/core/components/search/SearchResults.astro | CORE | Shared result list with vanilla JS rendering and variant prop ("card" default, "compact" for modal). Parent scope provides results, selectedIndex, onResultClick. |
src/core/components/search/SearchPage.astro | CORE | Dedicated search page logic with URL state (?q= via replaceState). |
src/core/components/search/SearchTrigger.astro | CORE | Dual-mode trigger: modal mode dispatches search-open event; page mode links to /search/. |
src/pages/search.astro | SITE | Thin route wrapper with PageGridLayout, noindex. |
Search Modal
Rendered in PageGridLayout when siteConfig.header.search.enabled && search.behavior === "modal". Communicates with SearchTrigger via a search-open custom DOM event on document.
The modal backdrop uses the shared --st-color-overlay-scrim semantic token at 50% alpha so overlay treatment stays consistent with other full-screen surfaces.
UX states
- initial — Modal open, no query entered.
- loading — First search in progress (Pagefind loading).
- results — Results displayed in a scrollable list.
- no-results — Query returned zero results.
- error — Pagefind failed to load or query threw.
Keyboard
- Cmd/Ctrl+K opens the modal (suppressed in inputs/textareas/contenteditable).
- Tab moves between input, result links, and close button.
- Escape closes the modal and returns focus to the trigger element.
Search Page
Available at /search/ when search.enabled is true. Supports ?q= URL parameter for direct linking. Same Pagefind integration as the modal but rendered inline.
Docs Index Integration
The docs index (/theme-docs/) uses a slug-match hybrid approach (D-C7-10): Pagefind returns text search results, which are matched by URL to the server-rendered items array. Faceted filters (topic, type, audience) remain as client-side post-filters. Empty queries show all items without invoking Pagefind.
Search Events
Five analytics event stubs are defined in search-events.ts. They emit console.debug in development and are no-ops in production. C11 (Analytics & Consent) will replace the dispatch mechanism without changing call sites.
| Helper | When fired |
|---|---|
trackSearchOpened(source) | Modal opens |
trackSearchSubmitted(queryLength) | Pagefind query completes |
trackSearchResultsViewed(count) | Results rendered |
trackSearchResultClicked(index) | User clicks a result |
trackSearchNoResults(queryLength) | Zero results returned |
Configuration
Search behavior is controlled via siteConfig.header.search:
search: {
enabled: true, // Show search trigger in header
behavior: "modal", // "modal" | "page"
href: "/search/", // Page mode link target
}
Pagefind Indexing
data-pagefind-body is placed unconditionally on the main-header div and #main-content in PageGridLayout.astro. This indexes page titles, subheadings, and body content while excluding nav, footer, and chrome.
Collection filters
Content is scoped to collections via data-pagefind-filter="collection:{value}" on <main>. PageGridLayout exposes a pagefindFilter prop that renders this attribute declaratively. Routes set pagefindFilter="page", "article", or "doc" to place content in a collection. Pages without a pagefindFilter value are indexed by Pagefind but excluded from filtered queries (e.g., the search page itself, 404).
Multi-value filter workaround
Pagefind treats multi-value filter arrays as AND (all values must match). search-client.ts works around this by running separate queries per filter value and merging results, deduplicating by result ID. This enables queries like { collection: ["page", "article"] } to return results from either collection.