Skip to content

Search

Markdown

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.

  1. Build.

    Terminal
    pnpm build

    It prints one line; N is the number of listing detail pages:

    [zerodirs:pagefind] indexed N pages → pagefind/

    The zero-js: line further down still ends — ok.

  2. Serve the build.

    Terminal
    pnpm preview

    Open http://localhost:4321/search/. The status line under the form reads Type at least 2 characters, or pick a filter, to search tools.

  3. 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, or Showing 10 of <n> tools past ten.

  4. 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=popularity and the back button gains nothing.

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

The search page on the demo: the query in the box, pricing and category filters, and the result list

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.

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