generateSearchIndexTask
Walk your rendered HTML pages and emit a lean JSON search index for client-side search — no external search service required.
When to Use
✅ Use generateSearchIndexTask when you want:
- Fast, native, client-side search over your site
- A stable JSON index (title, headings, body text, URL) a search UI can fetch
- Search built from the rendered pages, so it indexes exactly what readers see
It is the search counterpart to generateNavDataTask: same task shape and registration, but it reads rendered HTML rather than Markdown source, and it strips that HTML down to searchable text.
Run it after all page-generating tasks so it can see the finished pages.
Quick Start
generateSearchIndexTask({
scanDir: 'public',
outDir: 'public',
siteUrl: 'https://skier.ripixel.co.uk',
})Output: public/search-index.json
Configuration
| Option | Type | Required | Description |
|---|---|---|---|
scanDir |
string | ✅ | Directory to scan for rendered .html files |
outDir |
string | ✅ | Directory to write the index JSON into |
fileName |
string | Output filename (default: 'search-index.json') |
|
siteUrl |
string | Site root URL for absolute page URLs (default: root-relative) | |
contentSelector |
string | Tag whose contents are indexed, scoping out chrome (default: 'main'; use 'body' for the whole page). Falls back to <body> if absent. |
|
excludes |
string[] | Glob patterns to exclude (merged with defaults 404.html, 404/**) |
|
outputVar |
string | If set, the index is also exposed on globals[outputVar] for downstream tasks |
|
maxBodyLength |
number | Cap each page's body text to N characters (default: 0 = no limit) |
|
pretty |
boolean | Pretty-print the JSON (default: false — compact keeps the payload lean) |
Type signature
The exact GenerateSearchIndexConfig interface, transcluded from source so it stays in step with the shipping code:
export interface GenerateSearchIndexConfig {
/** Directory to scan for rendered `.html` pages */
scanDir: string;
/** Directory to write the search index JSON into */
outDir: string;
/** Output filename for the index (default: `'search-index.json'`) */
fileName?: string;
/** Site root URL for absolute page URLs (default: root-relative URLs) */
siteUrl?: string;
/**
* Tag name whose contents are indexed, scoping out shared chrome (sidebar,
* header, footer). Default: `'main'`. Use `'body'` to index the whole body.
* If the tag is not found on a page, the task falls back to `<body>`.
*/
contentSelector?: string;
/** Glob patterns to exclude from the index. Merged with defaults (`404.html`, `404/**`). */
excludes?: string[];
/** If set, the index object is also exposed on `globals[outputVar]` for downstream tasks. */
outputVar?: string;
/** Cap each page's body text to this many characters (default: `0` = no limit). */
maxBodyLength?: number;
/** Pretty-print the JSON (default: `false` — compact output keeps the payload lean). */
pretty?: boolean;
}Every task's config type together: API Reference → Config Interfaces.
What Gets Indexed
For each page the task extracts:
| Field | Source |
|---|---|
url |
Cleaned, extension-free page URL (index.html → /, about.html → /about) |
title |
First <h1> in the content region, falling back to <title>, then the filename |
headings |
Every <h1>–<h6> in document order, with level and (when present) an anchor id |
body |
The content region with HTML stripped, entities decoded, and whitespace collapsed |
To keep the payload lean and relevant, the task scopes to contentSelector (default <main>)
and strips <script>, <style>, HTML comments, and in-content <nav> blocks (breadcrumbs,
prev/next) before extracting text — so shared chrome like the sidebar, header, and footer is not
repeated on every page.
Output Structure
The real exported types, transcluded from source so this reference can't drift from the shipping contract:
/**
* A single heading extracted from a page, in document order.
*
* `id` is populated when the rendered heading carries an `id` attribute (as
* produced once heading-anchor support lands), so search results can deep-link
* straight to the relevant section. It is omitted when no anchor is present.
*/
export interface SearchHeading {
/** Heading text with all inline HTML stripped */
text: string;
/** Heading level, 1–6 (from `<h1>`–`<h6>`) */
level: number;
/** Anchor slug for deep-linking, when the heading has an `id` */
id?: string;
}
/**
- One entry in the search index: everything a client-side search UI needs to
- find and rank a page, and nothing it doesn't.
/
export interface SearchIndexEntry {
/* Clean, extension-free URL of the page (root-relative, or absolute if
siteUrl is set) /
url: string;
/* Page title (first <h1>, falling back to <title>, then filename) /
title: string;
/* Headings in document order, for section-level matching and result grouping /
headings: SearchHeading[];
/* Plain, HTML-stripped, whitespace-collapsed body text for full-text matching */
body: string;
}
/**
- The complete search index document written to disk as JSON.
- This format is a stable public contract consumed by the client-side search
- UI (a separate task). Add fields additively; do not rename or repurpose
- existing ones.
/
export interface SearchIndex {
/* All indexed pages, sorted by URL for deterministic output */
pages: SearchIndexEntry[];
}
This format is a stable contract consumed by the client-side search UI. Fields are added additively; existing fields are not renamed or repurposed. It's also documented in the API Reference → Core Types.
Example entry:
{
"url": "/getting-started",
"title": "Getting Started",
"headings": [
{ "text": "Getting Started", "level": 1 },
{ "text": "Install", "level": 2, "id": "install" }
],
"body": "Install with npm. Run the CLI to build your site. Install Requirements Node 22 or later is required."
}Exclusions
404.html and 404/** are always excluded. Add your own patterns via excludes — they are
merged with the defaults and logged at info level:
generateSearchIndexTask({
scanDir: 'public',
outDir: 'public',
excludes: ['drafts/**', 'admin/**'],
})ℹ Search index: excluded /404.html (matched pattern: 404.html)
ℹ Search index: excluded /drafts/wip.html (matched pattern: drafts/**)Pipeline Position
Place after every page-generating task, alongside the sitemap:
export default [
prepareOutputTask({ /* ... */ }),
generateItemsTask({ /* ... */ }),
generatePagesTask({ /* ... */ }),
generateSitemapTask({ scanDir: 'public', outDir: 'public' }),
generateSearchIndexTask({ scanDir: 'public', outDir: 'public' }), // ← after pages exist
];Related Tasks
- generateNavDataTask — The task this one is modelled on (sidebar data)
- generateSitemapTask — Same scan-and-clean-URL approach, for SEO
- generateItemsTask — Renders the pages this task indexes