Instant client-side search across pages and posts, indexed at build time.
A search box that answers as you type. It searches every published page and post — titles first, then body text — entirely in the visitor's browser. No search service, no server round-trips, nothing to pay for.
A small script queries the content API at build time, flattens each entry's block-based body to plain text, and writes a JSON index into the frontend's public folder. A client component loads that index on first focus and ranks matches with simple field-weighted scoring. Republishing content rebuilds the index via the publish webhook (or on the next build).
You are adding client-side search to a Prelo project. Read `prelo docs api` for the content API shape.
1. **Index script.** Create `web/scripts/build-search-index.mjs`:
- Fetch `GET {CMS_URL}/api/page` and `GET {CMS_URL}/api/post` (add any other public types the project defines — read `GET /api/schema` to enumerate them). Unauthenticated fetch is correct: it returns published entries only. Paginate with `per_page=200` until `total` is covered.
- For each entry, flatten `body` (an array of blocks `{type, variant, data}`) to plain text: for `text` blocks take `data.text`; for other block types concatenate any string values in `data`. Strip HTML tags from richtext fields.
- Write `web/public/search-index.json` as an array of `{ title, url, type, text }`, where `url` follows the project's routing (usually `/{slug}` for pages and `/blog/{slug}` or `/posts/{slug}` for posts — check the frontend's routes and match them exactly).
- Cap `text` at ~2,000 characters per entry to keep the index small.
2. **Wire it into the build.** Add the script to the frontend's build as a pre-step (`prebuild` npm script, or the project's equivalent). It must not fail the build if the CMS is unreachable — warn and keep the previous index file if one exists.
3. **Search component.** Add a `SiteSearch` client component:
- Input with a listbox of results underneath; fetch `/search-index.json` lazily on first focus and cache it.
- Matching: case-insensitive substring over title and text. Score: title match = 3 points, text match = 1; sort descending, show top 8 with title, type label, and a ~120-character snippet around the first match.
- Debounce input ~120 ms. Full keyboard support: arrow keys move the active result, Enter navigates, Escape closes. Use `role="listbox"`/`option` and `aria-activedescendant`.
- Empty query shows nothing; no matches shows "No results for '…'".
4. **Placement.** Put the component where the user asks — typically the site header. Keep styling consistent with the project's existing header controls.
5. **Fresh index on publish.** If the project deploys to a host that rebuilds on webhook (Vercel/Netlify deploy hook), set `webhooks: { onPublish: "<deploy-hook-url>" }` in `cms.config.js` so publishing re-triggers the build and re-index. For a long-running self-hosted frontend, re-run the index script on a timer or on the webhook instead.
6. **Verify.** Build, confirm `search-index.json` exists and contains published entries only, and test a query that matches a body but not a title.Search just works for visitors. When the client publishes new content in Studio, the next build (or webhook-triggered rebuild) picks it up — nothing to manage.
Threaded comments with a moderation queue in Studio. No third-party embed.
Events as a content type with a monthly calendar view and iCal feed.
Contact form with spam protection and email notifications, plus a submissions inbox in Studio.