Wiki Search
- internal/wiki/search.go
- internal/wiki/search/search.js
Relevant source files · 5
Astrolabe bundles are static sites, so full-text search cannot lean on a server. Instead, the pipeline ships a client-side search index built with Pagefind: after the bundle is written, the pagefind CLI crawls the rendered HTML and produces a static index under <bundle>/pagefind/ File overview. In the reader's browser, a small self-contained dialog script (assets/search.js, embedded into every bundle) talks to that index directly — no backend, no network calls beyond the bundle's own files search.js header.
The whole feature is deliberately optional and progressive: a bundle generated without pagefind on PATH is still complete and readable — the search button simply never appears search.js header, because the button in the site header ships hidden and is only revealed once the script confirms the index is reachable BaseLayout.astro.
How it works
1. The operator chooses a mode. SearchMode selects whether a bundle gets a search index at all: off (no index), auto (index when pagefind is installed), or require (index, and fail the build without it) declaration. The CLI exposes this as the --search flag on generate, defaulting to auto via the ASTROLABE_SEARCH environment variable flag definition. cmdGenerate parses the flag value with wiki.ParseSearchMode before anything else runs, exiting with an error on an unknown mode parsing. ParseSearchMode accepts off/auto/require case-insensitively and treats an empty string as off ParseSearchMode.
2. The pipeline indexes after writing the bundle. The flag does not stay in the CLI layer: the parsed mode is passed into agent.Config.Search agent config, and the agent's Run pipeline calls wiki.IndexSearch(ctx, outDir, a.cfg.Search, a.log) immediately after w.Write(site) succeeds and before the incremental state is written IndexSearch call in Run. If indexing fails in require mode, the whole run fails — the bundle is not silently shipped half-searchable.
3. IndexSearch rebuilds the index. The function short-circuits immediately for off (or an empty mode) early return. Otherwise it looks up the Pagefind executable — the binary name is the package variable PagefindBin ("pagefind"), looked up on PATH, so tests can substitute a fake PagefindBin. When the lookup fails, the two modes diverge exactly as their names promise: require returns an actionable error suggesting npm install -g pagefind or --search off, while auto just logs a warning ("the bundle has no search index") and returns nil LookPath handling.
Once the binary is found, the existing <dir>/pagefind directory is removed first — the const SearchDir names that directory ("pagefind") — so pages dropped since the previous build do not linger in a stale index stale-index removal. The CLI is then run as pagefind --site <dir> under a 10-minute timeout, wrapped in an OpenTelemetry span recording the command arguments, duration, exit code, and the number of pages indexed span + command. Pagefind's output is parsed for its "Indexed N page(s)" line to extract that count indexedRe. Two failures are treated as errors and recorded on the span: the process exiting nonzero (with its output attached to the error), and the degenerate case where pagefind exits cleanly but indexed zero pages — which would produce a search box that can never find anything failure handling. Success logs the page count and duration success log.
4. Only wiki articles are indexed. The theme marks the article element with data-pagefind-body only when the page opts in via the layout's indexed prop, so navigation, headers, and the index page stay out of the index — hits come from page content, not page chrome data-pagefind-body.
5. The browser side: assets/search.js. The dialog script is embedded into every bundle and copied to assets/search.js by the writer's asset step embed. Its startup logic is a reachability probe, not an assumption: it reads its URL prefix from the data-root attribute on its own <script> tag (so bundles work under any URL prefix), does a HEAD request against pagefind/pagefind-entry.json, and only if that succeeds does it un-hide the header button and wire up openers — the button click, Ctrl/Cmd+K, or / probe + enable. On non-HTTP origins (e.g. a file:// bundle) the script exits entirely protocol guard.
Opening the dialog lazily builds its DOM (a native <dialog> with a search input, a results list, and a status line), then loads the Pagefind runtime once via dynamic import of pagefind/pagefind.js relative to the bundle root lazy load. Each keystroke runs pf.debouncedSearch with a sequence guard: a monotonically increasing query number ensures that only the result of the latest query is ever rendered, so out-of-order responses from slow lookups can't clobber fresh ones run with seq guard. Rendering caps results at ten, shows a title plus a type badge and excerpt per hit, and appends up to three "sub-results" (section-level matches on the same page) beneath each entry render. The excerpt is inserted as HTML because Pagefind pre-escapes page text, leaving only its own <mark> highlighting as markup excerpt note.
Keyboard navigation in onKey moves an aria-selected highlight with the arrow keys (wrapping around the list) and navigates with Enter — either the highlighted hit or, if none is selected yet, the first one onKey. Closing is handled outside onKey: the Esc kbd hint in the search box is a clickable element that calls dialog.close(), clicking the backdrop does the same, and Escape works natively because the widget is a standard <dialog> close handlers.
Error handling
| Situation | auto |
require |
|---|---|---|
pagefind not on PATH |
warning logged, build succeeds without an index LookPath handling | hard error with install advice require error |
| pagefind exits nonzero | error, run fails | error, run fails |
| pagefind indexes 0 pages | error, run fails | error, run fails |
ParseSearchMode rejects anything other than off/auto/require with a message naming the valid values ParseSearchMode.
Relationships
graph TD
CLI["cmdGenerate<br/>(cmd/astrolabe/main.go)"] -->|"parses --search"| Parse["wiki.ParseSearchMode<br/>(internal/wiki/search.go)"]
CLI -->|"Config.Search"| Agent["agent.Agent.Run<br/>(internal/agent/agent.go)"]
Agent -->|"after Writer.Write"| Index["wiki.IndexSearch<br/>(internal/wiki/search.go)"]
Index -->|"executes"| PF["pagefind --site <bundle>"]
Index -->|"removes stale"| Dir["<bundle>/pagefind (SearchDir)"]
Writer["wiki.Writer / writeAssets<br/>(internal/wiki/design.go)"] -->|"embeds searchFS"| JS["search.js<br/>(internal/wiki/search/search.js)"]
Layout["theme BaseLayout.astro"] -->|"data-pagefind-body"| Index2["Pagefind index"]
JS -->|"import + HEAD probe"| Index2
Reader["Reader's browser"] -->|"Ctrl/Cmd+K, /, button"| JSReference
| Symbol | Kind | What it does | Location |
|---|---|---|---|
SearchMode |
type | off / auto / require; selects whether a bundle gets an index |
src/internal/wiki/search.go#L27-L34 |
ParseSearchMode |
function | Parses the flag value; empty means off |
src/internal/wiki/search.go#L41-L49 |
PagefindBin |
variable | Pagefind executable name, looked up on PATH (overridable for tests) |
src/internal/wiki/search.go#L52-L53 |
SearchDir |
const | Index directory name inside the bundle (pagefind) |
src/internal/wiki/search.go#L55-L56 |
IndexSearch |
function | Rebuilds <bundle>/pagefind; per-mode failure policy, OTel span |
src/internal/wiki/search.go#L61-L121 |
searchFS |
variable | Embedded dialog script, copied to assets/search.js |
src/internal/wiki/design.go#L36-L37 |
--search |
CLI flag | auto (default, via ASTROLABE_SEARCH), require, or off |
src/cmd/astrolabe/main.go#L157 |
enable (search.js) |
function | HEAD-probes the index, then reveals the search button and openers | src/internal/wiki/search/search.js#L68-L82 |
run (search.js) |
function | Debounced Pagefind query with a latest-wins sequence guard | src/internal/wiki/search/search.js#L84-L101 |
render (search.js) |
function | Renders top-10 hits with badges, excerpts, and sub-results | src/internal/wiki/search/search.js#L103-L124 |
onKey (search.js) |
function | Arrow-key navigation and Enter-to-navigate over the result list | src/internal/wiki/search/search.js#L126-L141 |
Generated by astrolabe from commit ecc180b3 (tag 0.4.0).