Static Search
ZeroPress can emit dependency-free native search artifacts during a normal build:
/_zeropress/search.json/_zeropress/search.js/_zeropress/search_pagefind.js
The theme owns the search UI. The generated adapter owns loading, tokenizing, scoring, and returning search results.
Native search is enabled only when both conditions are true:
- preview-data does not set
site.search: false - the active theme declares
features.search: true
When native search is disabled, ZeroPress does not emit /_zeropress/search.json, /_zeropress/search.js, or /_zeropress/search_pagefind.js, and templates receive site.search: false so search UI can be hidden with {{#if site.search}}.
Native Search
Import the adapter from client-side theme JavaScript:
const searchApi = await import("/_zeropress/search.js");
const result = await searchApi.search("ubuntu", { limit: 10 });
for (const item of result.results) {
const data = await item.data();
console.log(data.meta.title, data.url, item.score);
}
The result shape intentionally resembles a small subset of Pagefind:
{
results: [
{
id: "post:hello-world",
score: 12.4,
data: async () => ({
url: "/posts/hello-world/",
excerpt: "Short excerpt...",
plain_excerpt: "Short excerpt...",
meta: {
title: "Hello World",
type: "post",
published_at_iso: "2026-05-01T00:00:00Z",
updated_at_iso: "2026-05-02T00:00:00Z",
categories: ["Guide"],
tags: ["ZeroPress"]
},
sub_results: []
})
}
]
}
Only options.limit is supported in the native adapter. The default limit is 20.
Search UI Hooks
Themes own the search UI. Use these hook names when wiring client-side search
behavior so ZeroPress themes remain easier to compare and adapt:
{{#if site.search}}
<form role="search" data-zp-search>
<input type="search" data-zp-search-input>
<button type="submit" data-zp-search-submit>Search</button>
<p data-zp-search-status></p>
<div data-zp-search-results></div>
</form>
{{/if}}
These hooks are conventions for theme JavaScript. ZeroPress does not
automatically attach UI behavior to them; the theme still imports
/_zeropress/search.js and renders results.
Indexed Content
search.json indexes published posts and pages. It does not index taxonomy routes, archive routes, 404.html, feed files, sitemap files, or standalone raw front-page HTML.
When the front page is rendered from a page, that page is indexed with the URL /.
Posts or pages with discoverability: "delist" are excluded from the native search index. site.indexing: false does not disable native search; it only changes generated fallback robots.txt. Use site.search: false to disable native search artifacts.
Each search item includes:
{
"id": "post:hello-world",
"type": "post",
"title": "Hello World",
"url": "/posts/hello-world/",
"excerpt": "Short excerpt...",
"headings": ["Install", "Usage"],
"categories": ["Guide"],
"tags": ["ZeroPress"],
"published_at_iso": "2026-05-01T00:00:00Z",
"updated_at_iso": "2026-05-02T00:00:00Z",
"content_text": "Plain searchable body text"
}
content_text is derived from rendered HTML after removing scripts, styles, tags, entities, and extra whitespace.
Scoring
The native adapter uses a small BM25-like and TF-IDF-like scorer.
Current field weights are internal defaults:
title:5headings:3tags:2.5categories:2excerpt:1.5content_text:1
Posts can receive a small recency boost. The maximum recency boost is 0.15, so it should not overpower a clearly better text match.
These values are not preview-data or theme runtime configuration. Treat them as ZeroPress-owned defaults for the native adapter.
Pagefind
For larger sites or higher search quality, Pagefind is a good post-build option:
npx pagefind@latest \
--site ./_site \
--output-subdir _zeropress/pagefind
Then replace the native adapter with the generated Pagefind adapter:
cp ./_site/_zeropress/search_pagefind.js ./_site/_zeropress/search.js
rm ./_site/_zeropress/search.json
Removing search.json is optional; it only removes the unused native index from a Pagefind deployment.
Pagefind writes its own /_zeropress/pagefind/ assets after the ZeroPress build. The theme can keep importing the same adapter path:
const searchApi = await import("/_zeropress/search.js");
Use data-pagefind-body on the actual post/page body wrapper instead of relying on a broad --root-selector such as .prose or article:
<div
class="prose"
{{#if site.search}}{{#if_neq page.discoverability "delist"}}data-pagefind-body{{/if_neq}}{{/if}}
>
{{page.html}}
</div>
Do not add data-pagefind-body to layout-wide wrappers, archive pages, tag pages, category pages, or 404 pages unless those routes are intentionally searchable.