Search
Search is Pagefind: an index built once, at pnpm build, from the
rendered listing pages, served as static files and queried in the browser. features.search
turns it on and is true by default. /search/ is the only page on the site that runs client
JavaScript; everywhere else the search box is a plain form that lands there.
-
Build.
Terminal pnpm buildIt prints one line;
Nis the number of listing detail pages:[zerodirs:pagefind] indexed N pages → pagefind/The
zero-js:line further down still ends— ok. -
Serve the build.
Terminal pnpm previewOpen
http://localhost:4321/search/. The status line under the form readsType at least 2 characters, or pick a filter, to search tools. -
Search with the query in the URL.
Open
http://localhost:4321/search/?q=writing. The index loads before you touch the box, and the status line counts the results:<n> tools, orShowing 10 of <n> toolspast ten. -
Filter without a query.
Open
http://localhost:4321/search/?pricing=free. Results appear with nothing typed. Change the sort to Most popular: the address bar gains&sort=popularityand the back button gains nothing.
Verify
Section titled “Verify”| Where | You should see | |
|---|---|---|
| The bundle | ls dist/client/pagefind/ |
pagefind.js, pagefind-entry.json and the index/, fragment/ and filter/ directories |
| One island, on one page | the build log | zero-js: <n> content pages, 0 islands, 0 external scripts — ok |
| A result | /search/?q=<a listing name> |
that listing first: logo or letter tile, name, pricing and category badges, tagline |
| A shared link | copy the address bar after filtering, open it in a new tab | the query, the filters and the sort come back |
| Only listings | /search/?q=<a category name> |
listings from that category; never the category page itself |

How it works
Section titled “How it works”What is indexed. Only listing detail pages carry data-pagefind-body, on the <main> that
wraps src/components/listing/Detail.astro; category, tag, generated and blog pages are not in
the index. Inside a detail page, “Related”
and “Appears in” are marked data-pagefind-ignore, so an excerpt is the listing’s own text.
Each indexed page costs one fragment file against the file budget, which is why the index stops
at listings.
What a result carries. The logo is data-pagefind-meta="image[src], image_alt[alt]"; the
tagline is data-pagefind-meta="tagline" with weight 2, and the panel ranks it the same way
with metaWeights: { tagline: 2 }. A hidden span on every detail page carries
data-pagefind-sort="popularity[data-pop], date[data-date]"; a page without it drops out of
sorted results.
Filters, sorts and the URL. The five parameters are read once on mount and written back
with history.replaceState, empty values and sort=relevance omitted, so a filtered search is
a link you can share and browsing never fills the back button.
| Parameter | Values | Notes |
|---|---|---|
q |
text, trimmed, up to 200 characters | under 2 characters the filters search alone |
category, tag, pricing |
a slug from site.config.ts |
the index stores the visible name; the island maps slug to name before each call, so a renamed category keeps old links working. Anything that is not slug-shaped is dropped |
sort |
relevance (default), popularity, date |
shown as Relevance, Most popular, Newest. popularity is the listing’s popularity, in D1 mode the 30-day click count |
Category and tag pages link in with ?category=<slug> and ?tag=<slug>.
The island. src/islands/SearchPanel.tsx (Preact) mounts with client:idle on /search/
and is the only file in src/islands/. It imports /pagefind/pagefind.js on the first focus of
the box, or on mount when the URL already carries state, so a visitor who never searches
downloads nothing. Every keystroke calls preload(); the search itself is debounced at 250 ms
and superseded responses are discarded. The constants are SEARCH_MIN_QUERY = 2,
SEARCH_MAX_RESULTS = 10 and SEARCH_META_WEIGHTS in src/lib/search.ts, and DEBOUNCE_MS
in the island.
The zero-JS rule. After the build, integrations/budget.ts reads every page in
dist/client and fails the build if any page other than /search/ contains an
<astro-island> or a <script src>; /search/ is exempt by its route kind in the manifest. The two inline scripts on every page, the theme bootstrap and the toggle, are
allowed, and <script type="application/ld+json"> is structured data, not JavaScript.
/search/ is noindex and disallowed in robots.txt.
When it is off or degraded
Section titled “When it is off or degraded”| Config state | The visitor sees | /admin/ reports |
Fix |
|---|---|---|---|
features.search: false |
no search box in the header, on the home page (Browse all tools and Categories buttons take its place) or on the 404 page, and no Search within button on category and tag pages. /search/ itself is still built, island included: the page has no feature guard, only its links and its manifest entry go. And because it has left the manifest, the zero-JS check no longer exempts it, so the build stops with content pages must ship zero JavaScript: 1 of <n> page(s) carry an island or a downloaded script naming /search/ |
nothing — search has no runtime | leave the flag on until the page is guarded; it costs nothing at request time |
pnpm dev |
Search is not available yet. Run `pnpm build` once to enable search in dev. |
nothing | pnpm build, then pnpm preview. Nothing serves the built index to the dev server |
| The index failed to write | a build warning, indexing failed (…); build continues without pagefind/, never a failure; the panel shows the same message as in dev |
nothing | read the warning; the usual cause is the Pagefind binary for your platform |
No page carries data-pagefind-body |
no page with data-pagefind-body found in …; writing an empty index so /search/ still loads; every search finds nothing |
nothing | the detail template lost its pagefindBody() spread |
| The file budget | budget: warns above 80% of the plan limit and fails the build above 90% |
nothing | each listing costs one fragment on top of its page, plus the index chunks; see Cost and limits |
Configuration
Section titled “Configuration”| Field | Default | What it changes |
|---|---|---|
features.search |
true |
the header, home and 404 search forms, the Search within buttons, and the /search/ entry in the routes manifest |
See features. There is no search block: the minimum query
length, the result cap and the tagline weight are constants in src/lib/search.ts, and the
debounce is in src/islands/SearchPanel.tsx.
- integrations/pagefind.ts builds the index at
astro:build:done; warnings, never failures Directorysrc/
- islands/SearchPanel.tsx the island: lazy import, filters, sort, URL state
- pages/search.astro the only page with an island; passes the filter options as props
- lib/search.ts the attribute helpers, the URL parser and the constants
- components/listing/Detail.astro the indexed body: meta, filters and the sort carrier