Adding Search
Add client-side search to your Saga site.
Overview
Since Saga generates static HTML, search needs to happen client-side using a JavaScript library. There are two main approaches:
Binary index — these tools have a CLI that indexes your built site and produces a compact binary index, keeping bandwidth low.
- Pagefind — indexes your built HTML and provides a fast search UI with no server required.
- tinysearch — Rust-compiled-to-WebAssembly search with a very small footprint (~50kB for a typical blog), though it only matches complete words.
JSON index — instead of a binary index, these libraries rely on a JSON index that you build yourself (for example by using a Saga writer). These indexes can grow quite large on content-heavy sites.
- Lunr.js — a lightweight, fully client-side search library.
- Fuse.js — a fuzzy-search library that works well for smaller sites.
- MiniSearch — a tiny, zero-dependency JS library with fuzzy matching and auto-suggestions.
This guide walks through integrating Pagefind.
Install Pagefind
Add Pagefind to your project via npm or pnpm:
$ pnpm init
$ pnpm add pagefind
Run Pagefind after the build
Use afterWrite(_: to run Pagefind after each build:
import Foundation
try await Saga(input: "content", output: "deploy")
.register(
folder: "articles",
metadata: ArticleMetadata.self,
readers: [.parsleyMarkdownReader],
writers: [.itemWriter(swim(renderArticle))]
)
.afterWrite { _ in
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
process.arguments = ["pnpm", "pagefind", "--site", "deploy"]
try process.run()
process.waitUntilExit()
}
.run()
Pagefind writes the binary index plus the client-side JS library to deploy/pagefind.
Create a search page
Use createPage(_: to add a search page:
try await Saga(input: "content", output: "deploy")
.register(/* ... */)
.afterWrite { /* ... */ }
.createPage("search/index.html", using: swim(renderSearch))
.run()
The search template loads Pagefind’s UI and wires it up:
func renderSearch(context: PageRenderingContext) -> Node {
html {
head {
script(src: "/pagefind/pagefind-modular-ui.js")
script {
Node.raw("""
window.addEventListener('DOMContentLoaded', () => {
const q = new URLSearchParams(window.location.search).get("q");
const instance = new PagefindModularUI.Instance();
instance.add(new PagefindModularUI.Input({ inputElement: "#search" }));
instance.add(new PagefindModularUI.ResultList({ containerElement: "#results" }));
if (q) {
document.getElementById("search").value = q;
instance.triggerSearch(q);
}
});
""")
}
}
body {
h1 { "Search" }
form(action: "/search/") {
input(id: "search", name: "q", placeholder: "Search articles", type: "text")
}
div(id: "results")
}
}
}
Tip In a real project you’d want to share the base layout with the rest of your site. See Reusable HTML Layouts for how to set that up.
Check the source of loopwerk.io for a complete working search implementation, including controlling what Pagefind indexes.