Astrolabe / ajxpublic /astrolabe
Repository

Wiki Search

reference scope internal/wiki/search Verification failed
  • 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 &lt;bundle&gt;"]
    Index -->|"removes stale"| Dir["&lt;bundle&gt;/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"| JS

Reference

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).