The search experience on this site needed to feel like the rest of the theme: clean, typographic, and aligned with the post list layout. The default Pagefind UI is great for quick setup, but it brings its own markup and styles. Instead, the search page uses the Pagefind API directly, pulls JSON results, and renders the results using the same post card structure used elsewhere.

You can see it in action on the search page .
This post walks through the key ideas in themes/coreydaley-dev/layouts/_default/search.html and how the custom flow works. You can view the full file on GitHub
: search.html
.
Why Skip The Default UI
The stock Pagefind UI gives you a search box and results list out of the box, but that HTML structure doesn’t match the theme’s post list markup. The goal here was to keep the search results visually consistent: dates in the same format, taxonomy chips in the same place, separators between posts, and familiar “Read more” links.
Load Pagefind As A Module
The page imports the core Pagefind API (not the UI bundle) directly:
- The module import loads
/pagefind/pagefind.js. - That gives a
pagefind.search(query)call that returns a lightweight result list.
<script type="module">
const pagefind = await import("/pagefind/pagefind.js");
</script>
This is the foundation for custom rendering. You keep the search engine, but own the markup.
Turn Results Into Clean JSON
performSearch(query) is the main entry point. After calling pagefind.search(query), it fetches full result data with:
search.results.map((result) => result.data())
const search = await pagefind.search(query);
const fullResults = await Promise.all(
search.results.map((result) => result.data()),
);
Those result objects include URL, metadata, excerpt, and word count. The code then maps them into a clean JSON shape that’s easy to render:
title,description, andexcerptfor contenttagsandcategoriessplit into arraysdateandwordCountfor metadata
const jsonResults = fullResults.map((result) => ({
url: result.url,
title: result.meta.title || "Untitled",
description: result.meta.description || "",
excerpt: result.excerpt,
tags: result.meta.tags ? result.meta.tags.split(", ") : [],
categories: result.meta.categories ? result.meta.categories.split(", ") : [],
date: result.meta.date || "",
wordCount: result.word_count,
}));
This step is the big win: the JSON object is decoupled from Pagefind’s internal shape, so the rendering logic stays readable and theme-specific.
Render Results Using The Site’s Post List Styles
displayResults(results) builds the HTML using the same visual components as the rest of the site:
- Each result becomes an
<article class="post-item">. - Dates are formatted in US English with
toLocaleDateString. - Categories and tags are rendered as taxonomy links, using the same iconography and classes already used across the theme.
- A
Read more →link finishes the card.
article.innerHTML = `
${dateStr ? `<div class="post-date">${dateStr}</div>` : ""}
<h2 class="post-title">
<a href="${result.url}">${result.title}</a>
</h2>
${result.description ? `<p class="post-excerpt">${result.description}</p>` : ""}
${taxonomyHtml}
<a href="${result.url}" class="read-more">Read more →</a>
`;
Because the markup matches the site’s list layout, the search results feel like native content — not a bolted-on widget.
Small UX Details That Add Polish
A few subtle touches help the page feel responsive and intentional:
- A debounced search (300ms) avoids hammering the index as users type.
- Empty state messages (“Enter a search query…”, “No results found.”) provide guidance.
- The
qquery parameter syncs with the input so search results are linkable and shareable.
searchInput.addEventListener("input", (e) => {
const query = e.target.value;
debouncedSearch(query);
const newUrl = new URL(window.location);
if (query) newUrl.searchParams.set("q", query);
else newUrl.searchParams.delete("q");
window.history.replaceState({}, "", newUrl);
});
The Result: Pagefind Power, Theme-Native Presentation
The key idea is simple: Pagefind handles indexing and search relevance, while the site controls the UI. By converting Pagefind results into a clean JSON structure and rendering them with existing post styles, the search page keeps visual consistency without sacrificing search quality.
If you want to explore or extend this, start in themes/coreydaley-dev/layouts/_default/search.html and follow the performSearch → displayResults flow. That separation is what makes the implementation both flexible and easy to maintain.
