Search results
Query input, filter chips and result list.
The live search page at /search/. A single input fetches a JSON index, filters client-side on title, excerpt and collection, and renders matches as a key-value list.
Anatomy
- Search form — a
<search>wrapper containing a<form role="search" class="card">with a label and an autofocusedtype="search"input. - Results region —
<section id="results">holds the list; rendered as aul.key-value. - Result item — a
<li class="key-value--item key-value--search">with a link, collection label, title, excerpt and optional image. - No-match state — a
<p class="no-results">No results found</p>when the filter returns nothing.
Search form
<search>
<form role="search" aria-label="Search for a post" class="card">
<label class="label" for="searchBox">Search</label>
<input
type="search"
id="searchBox"
placeholder="Enter a search term"
aria-autocomplete="list"
aria-controls="results"
autofocus
>
</form>
</search>
Results
Results render into the same key-value layout the rest of the site uses for indexes. Each row carries the collection as the label, the title as the key, and the excerpt as the description. When an entry has an image — post hero, file icon or decorative object — it's prepended inside the link.
<section id="results">
<ul id="search-results" aria-label="Search results" class="key-value" role="listbox">
<h2 class="key-value--heading">Results</h2>
<li class="key-value--item key-value--search key-value--with-spacing">
<a class="key-value--link key-value--card-link" href="/posts/…">
<p class="key-value--label">Blog post</p>
<p class="key-value--key">Eurovision 2024 — a year of Northern Lights</p>
<p class="key-value--description">…</p>
</a>
</li>
</ul>
</section>
No results
Results
No results found
Templates
The layout uses native <template> elements that the client-side script clones into the DOM — no framework needed.
<template id="search-result-template">
<li class="key-value--item key-value--search key-value--with-spacing">
<a class="key-value--link key-value--card-link">
<p class="key-value--label"></p>
<p class="key-value--key"></p>
<p class="key-value--description"></p>
</a>
</li>
</template>
<template id="search-result-image-template">
<img class="key-value--image">
</template>
Index
A JSON file at /search.json lists every indexable page — title, date, URL, excerpt, optional image, and collection name. The script fetches it once and filters in memory.
async function loadIndex() {
const res = await fetch('/search.json');
return res.json();
}
function searchPosts(posts, query) {
const q = query.toLowerCase().trim();
if (q === '') return [];
return posts.filter(p =>
(p.title && p.title.toLowerCase().includes(q)) ||
(p.excerpt && p.excerpt.toLowerCase().includes(q)) ||
(p.collection && p.collection.toLowerCase().includes(q))
);
}
Accessibility
- Wrap the form in
<search>and give itrole="search". - Connect the input and result list with
aria-controlsandaria-autocomplete="list". - The result list is labelled
aria-label="Search results".