Welcome to Chapter 14! In this installment, we’ll elevate the usability of our static site generator by implementing powerful, client-side search capabilities. While our SSG is excellent for generating static content, a modern website often requires a way for users to quickly find specific information. We’ll integrate Pagefind, a fast and efficient search library designed specifically for static sites, to provide an intuitive search experience without needing a backend server.
This chapter will guide you through adding Pagefind to our build pipeline. We’ll learn how to configure our Rust SSG to execute Pagefind after all HTML content has been generated, allowing it to crawl our output directory and create a search index. Subsequently, we’ll integrate the necessary JavaScript and CSS assets into our Tera templates to enable the search interface on the frontend. By the end of this chapter, your generated site will feature a fully functional search bar, significantly improving content discoverability.
To follow along, ensure you have a working build of our SSG from Chapter 13, where we established the core rendering and output generation. We’ll be modifying the Builder structure and potentially some Tera templates. The expected outcome is a static website that, upon a rebuild, includes Pagefind’s search assets and allows users to search content directly within their browser.
Planning & Design
Integrating a tool like Pagefind into an SSG requires careful consideration of the build pipeline. Pagefind operates on already generated HTML files. This means it must run after our SSG has completed its primary task of rendering all content and writing it to the output directory (e.g., public/).
Our existing build process looks something like this:
- Load configuration.
- Scan content files.
- Parse content (frontmatter, Markdown to AST, component resolution).
- Render HTML using Tera templates.
- Write rendered HTML to the output directory.
We’ll insert the Pagefind execution as a post-processing step after step 5.
Component Architecture for Search Integration
The core changes will involve:
BuilderStructure: Adding a method or modifying the existingbuildmethod to orchestrate the Pagefind execution.ConfigStructure: Potentially adding Pagefind-specific configuration, like enabling/disabling it or specifying its executable path if not globally available.- Frontend Templates: Modifying our base Tera template (e.g.,
base.html) to include Pagefind’s client-side assets (CSS and JavaScript) and a search input element.
Here’s a high-level overview of the updated build process:
Pagefind Installation
Pagefind is an external command-line tool written in Rust, but we’ll interact with it via our SSG’s build process. You’ll need to install it globally or ensure it’s available in your project’s PATH.
Installation (via Cargo):
cargo install pagefind
Verify the installation:
pagefind --version
You should see a version number (e.g., pagefind v1.0.0).
Step-by-Step Implementation
First, let’s update our Builder and add configuration.
a) Setup/Configuration
We’ll add a new field to our Config struct to control Pagefind integration.
File: src/config.rs
// ... existing imports ...
use serde::Deserialize; // Add this import if not already present
#[derive(Debug, Deserialize, Clone)]
pub struct SiteConfig {
pub base_url: String,
pub title: String,
// ... other fields ...
#[serde(default = "default_pagefind_enabled")]
pub pagefind_enabled: bool,
#[serde(default = "default_pagefind_output")]
pub pagefind_output_dir: String,
}
impl Default for SiteConfig {
fn default() -> Self {
SiteConfig {
base_url: "http://localhost:8000".to_string(),
title: "My Awesome Site".to_string(),
// ... other default fields ...
pagefind_enabled: default_pagefind_enabled(),
pagefind_output_dir: default_pagefind_output(),
}
}
}
fn default_pagefind_enabled() -> bool {
true // By default, enable Pagefind
}
fn default_pagefind_output() -> String {
"pagefind".to_string() // Default output directory for Pagefind assets
}
// ... rest of config.rs ...
Explanation:
- We added
pagefind_enabled(defaulting totrue) to allow users to turn Pagefind off if they don’t need it. pagefind_output_dirspecifies where Pagefind will place its generated assets within ourpublicdirectory. This is useful for consistency and potential CDN configurations.#[serde(default = "...")]ensures these fields have default values if not specified inconfig.toml.
Now, update your config.toml to reflect these new options (or rely on defaults):
File: config.toml
base_url = "http://localhost:8000"
title = "My Awesome SSG Site"
# ... other config ...
[pagefind]
enabled = true
output_dir = "pagefind" # This will be public/pagefind/
b) Core Implementation - Integrating Pagefind into the Build
Next, we’ll modify our Builder to execute the pagefind command. We’ll use Rust’s std::process::Command for this.
File: src/builder.rs
use std::process::Command; // Add this import
// ... other imports ...
impl Builder {
// ... existing new, load_config, etc. methods ...
pub async fn build(&self) -> Result<(), Box<dyn Error>> {
info!("Starting site build...");
// 1. Ensure output directory is clean
let output_dir = &self.config.output_dir; // Assuming output_dir is a field in Config
if output_dir.exists() {
fs::remove_dir_all(output_dir)?;
info!("Cleaned output directory: {}", output_dir.display());
}
fs::create_dir_all(output_dir)?;
info!("Created output directory: {}", output_dir.display());
// 2. Load content and render pages
let site_data = self.load_site_data().await?; // Assuming this method exists and populates site data
let rendered_pages = self.render_all_pages(&site_data).await?;
// 3. Write rendered HTML to output directory
for (path, content) in rendered_pages {
let full_path = output_dir.join(&path);
if let Some(parent) = full_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&full_path, content)?;
debug!("Wrote page to: {}", full_path.display());
}
info!("Successfully rendered and wrote all pages.");
// 4. Run Pagefind if enabled
if self.config.site.pagefind_enabled {
info!("Pagefind integration enabled. Running Pagefind...");
self.run_pagefind().await?;
info!("Pagefind execution complete.");
} else {
info!("Pagefind integration disabled.");
}
info!("Site build complete!");
Ok(())
}
/// Executes the Pagefind CLI tool to generate a search index.
async fn run_pagefind(&self) -> Result<(), Box<dyn Error>> {
let output_dir = &self.config.output_dir; // Our main public/ directory
let pagefind_output_dir = output_dir.join(&self.config.site.pagefind_output_dir); // public/pagefind/
// Ensure Pagefind's output directory exists and is clean
if pagefind_output_dir.exists() {
fs::remove_dir_all(&pagefind_output_dir)?;
}
fs::create_dir_all(&pagefind_output_dir)?;
let command_args = vec![
"pagefind",
"--source",
output_dir.to_str().ok_or("Invalid output directory path")?,
"--output-path",
pagefind_output_dir.to_str().ok_or("Invalid Pagefind output path")?,
];
info!("Executing command: `pagefind {}`", command_args[1..].join(" "));
let output = Command::new(command_args[0])
.args(&command_args[1..])
.output()?;
if output.status.success() {
info!("Pagefind completed successfully.");
if !output.stdout.is_empty() {
debug!("Pagefind stdout: {}", String::from_utf8_lossy(&output.stdout));
}
} else {
error!("Pagefind failed with error code: {:?}", output.status.code());
if !output.stderr.is_empty() {
error!("Pagefind stderr: {}", String::from_utf8_lossy(&output.stderr));
}
return Err("Pagefind execution failed".into());
}
Ok(())
}
// ... existing helper methods like render_all_pages, load_site_data, etc. ...
}
Explanation:
- We added
use std::process::Command;to interact with external commands. - The
buildmethod now includes a conditional call toself.run_pagefind()after all pages are written. - The
run_pagefindasynchronous method constructs and executes thepagefindcommand. - It specifies
--sourceas our SSG’s main output directory (e.g.,public/) and--output-pathas the configuredpagefind_output_dir(e.g.,public/pagefind/). - Error handling is included:
- It checks
output.status.success()to determine if the command ran without errors. - It logs
stdoutandstderrfor debugging purposes. - It returns an
Errif Pagefind fails.
- It checks
- Before running Pagefind, we clean and recreate its specific output directory to ensure a fresh index.
c) Frontend Integration - Displaying Search
Pagefind generates a set of static assets (JavaScript, WASM, CSS) that need to be included in your site’s HTML to enable the search interface. We’ll modify our base Tera template.
File: templates/base.html (or your main layout template)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site.title }} - {% if page.title %}{{ page.title }}{% else %}Home{% endif %}</title>
<!-- Basic styling for demonstration -->
<style>
body { font-family: sans-serif; margin: 2em; line-height: 1.6; }
nav { margin-bottom: 2em; }
nav a { margin-right: 1em; }
.pagefind-ui {
margin-top: 2em;
border-top: 1px solid #eee;
padding-top: 1em;
}
</style>
<!-- Pagefind CSS -->
{% if site.pagefind_enabled %}
<link href="/{{ site.pagefind_output_dir }}/pagefind-ui.css" rel="stylesheet">
{% endif %}
</head>
<body>
<header>
<h1><a href="/">{{ site.title }}</a></h1>
<nav>
<a href="/">Home</a>
<!-- Add other navigation links here -->
</nav>
<!-- Search Input (optional, Pagefind UI can generate its own) -->
<div class="search-container">
{% if site.pagefind_enabled %}
<label for="search">Search:</label>
<input type="text" id="search" placeholder="Search site content...">
{% endif %}
</div>
</header>
<main>
{% block content %}{% endblock content %}
</main>
<footer>
<p>© 2026 {{ site.title }}</p>
</footer>
<!-- Pagefind JS -->
{% if site.pagefind_enabled %}
<script src="/{{ site.pagefind_output_dir }}/pagefind-ui.js" type="text/javascript"></script>
<script>
window.addEventListener('DOMContentLoaded', (event) => {
new PagefindUI({
element: "#search", // The input element to bind to
showImages: false,
highlightQuery: true,
// Add more options as needed for customization
// For example, to show results in a custom div:
// result_parent: "#search-results",
});
});
</script>
{% endif %}
</body>
</html>
Explanation:
- We’ve added conditional blocks
{% if site.pagefind_enabled %}to ensure Pagefind assets are only included if the feature is enabled in our configuration. <link href="/{{ site.pagefind_output_dir }}/pagefind-ui.css" rel="stylesheet">includes the Pagefind UI styling.<input type="text" id="search">provides a simple search input field. Pagefind’s UI library will attach to this.<script src="/{{ site.pagefind_output_dir }}/pagefind-ui.js" type="text/javascript"></script>loads the Pagefind UI JavaScript.- The
new PagefindUI(...)call initializes the search interface, binding it to our#searchinput element. You can customize its behavior and appearance extensively using the options provided.
d) Testing This Component
- Build your site: Run your SSG’s build command:
cargo run -- build - Verify Pagefind output:
- Check your
public/directory. You should see a new subdirectory,public/pagefind/(or whateverpagefind_output_diryou configured). - This directory should contain files like
pagefind-ui.css,pagefind-ui.js,pagefind.js,pagefind.wasm, and various index files (e.g.,pagefind.pf_idx).
- Check your
- Serve your site: Use a local web server to serve the
public/directory. A simple way is to useminiserve(install withcargo install miniserve) or Python’shttp.server:# From the project root, assuming public/ is your output miniserve public/ # or python3 -m http.server --directory public/ 8000 - Test search:
- Open your browser to
http://localhost:8000. - You should see the search input field.
- Type a word or phrase that exists in your content (e.g., “markdown”, “rust”, “component”).
- As you type, Pagefind should display search results dynamically below the input field.
- Open your browser to
Debugging Tips:
- “pagefind not found” error: Ensure
pagefindis installed and available in your system’sPATH. Runpagefind --versionin your terminal to confirm. - No search results / Pagefind assets missing:
- Check your SSG’s build logs for
Pagefind failedorPagefind stdout/stderrmessages. - Verify the
public/pagefinddirectory exists and contains the necessary files. - Inspect your browser’s developer console for any JavaScript errors related to
PagefindUIor network errors when trying to load/pagefind/pagefind-ui.jsor/pagefind/pagefind.wasm. Ensure the paths inbase.htmlmatch yourpagefind_output_dir. - Confirm
pagefind_enabled = truein yourconfig.toml.
- Check your SSG’s build logs for
Production Considerations
- Error Handling: Our
run_pagefindmethod already includes basic error handling by checking the command’s exit status. In a production system, you might want more robust error reporting (e.g., sending an alert, failing the CI/CD pipeline explicitly). - Performance Optimization:
- Build Time: For very large sites, Pagefind’s indexing step can add noticeable time to the build process. Pagefind is highly optimized, but it’s a trade-off for client-side search.
- Client-Side Performance: Pagefind assets (especially
pagefind.wasm) are loaded asynchronously and are generally small. The search is performed entirely client-side, making it very fast for users. Ensure your web server serves these static assets with appropriate caching headers. - Incremental Builds: In future chapters, when we implement incremental builds, we’ll need to consider how Pagefind integrates. Ideally, Pagefind would only re-index changed pages, but its current design is to re-index the entire output directory. This is a known limitation for SSGs focused on speed, but often acceptable given Pagefind’s efficiency.
- Security Considerations:
- Since Pagefind operates purely on static files and client-side JavaScript, the security implications are minimal. There’s no server-side component to attack.
- The main “security” concern is ensuring that sensitive content is not included in your static HTML if it shouldn’t be searchable. Pagefind will index whatever HTML it finds.
- Logging and Monitoring: Our
run_pagefindmethod logs Pagefind’sstdoutandstderr. In a CI/CD environment, ensure these logs are captured so you can diagnose build failures related to Pagefind.
Code Review Checkpoint
At this point, you should have:
- Modified
src/config.rs: Addedpagefind_enabledandpagefind_output_dirfields toSiteConfig. - Updated
config.toml: Included[pagefind]section withenabledandoutput_dir. - Modified
src/builder.rs:- Added
use std::process::Command;. - Integrated a call to
self.run_pagefind().await?into the mainbuildmethod. - Implemented the
run_pagefindasync method to execute thepagefindCLI tool.
- Added
- Modified
templates/base.html:- Added conditional
<link>forpagefind-ui.css. - Added an
<input type="text" id="search">element. - Added conditional
<script>forpagefind-ui.jsand its initialization code.
- Added conditional
This setup ensures that every time you run cargo run -- build, your site’s HTML is generated, and then Pagefind is executed to create a search index based on that HTML, with the necessary client-side assets injected into your templates.
Common Issues & Solutions
Issue:
error: process didn't exit successfully:pagefind …(exit code: 1)orPagefind execution failed.- Reason: The
pagefindcommand failed for some reason. This could be due to incorrect paths, permissions, or a problem within Pagefind itself. - Solution:
- Check the
stderroutput logged by ourrun_pagefindfunction. It usually contains the specific error message from Pagefind. - Verify the
--sourceand--output-patharguments inrun_pagefindpoint to valid, accessible directories. - Ensure the
pagefindexecutable is correctly installed and accessible (pagefind --version). - Try running the
pagefindcommand manually from your terminal with the same arguments to see the exact error.
- Check the
- Reason: The
Issue: Search input appears, but no results are shown, or JavaScript errors in the console like
PagefindUI is not defined.- Reason: Pagefind’s client-side assets (
pagefind-ui.js,pagefind.wasm) are not being loaded correctly by the browser. - Solution:
- Inspect your browser’s developer tools (Network tab) to see if
pagefind-ui.jsandpagefind.wasmare being requested and loaded successfully. Check their paths. - Ensure the paths in
templates/base.html(/{{ site.pagefind_output_dir }}/pagefind-ui.js) correctly resolve to the actual location of the Pagefind assets in yourpublic/directory. Remember the leading/for absolute paths. - Verify that
site.pagefind_enabledistruein yourconfig.tomland that the Tera template conditional renders the script tags. - Make sure
public/pagefind/(or your configured output directory) actually contains the Pagefind assets after a build.
- Inspect your browser’s developer tools (Network tab) to see if
- Reason: Pagefind’s client-side assets (
Issue: Pagefind builds successfully, but the search results are empty or incomplete.
- Reason: Pagefind might not be indexing all your content, or your content might not contain the keywords you’re searching for.
- Solution:
- Pagefind by default indexes the visible text content of HTML files. If parts of your content are dynamically loaded or hidden, they might not be indexed.
- Pagefind supports data attributes (
data-pagefind-filter,data-pagefind-meta,data-pagefind-body) to control what gets indexed. Refer to Pagefind documentation if you need more granular control over indexing specific elements or adding custom metadata to search results. For now, it should index all visible text. - Ensure your content files are actually being rendered to HTML in the
public/directory.
Testing & Verification
To thoroughly test the Pagefind integration:
- Clean Build:
rm -rf public(to ensure a completely fresh start)cargo run -- build- Verify
public/pagefind/exists and contains files.
- Local Server:
miniserve public/(orpython3 -m http.server --directory public/ 8000)
- Browser Tests:
- Open
http://localhost:8000. - Positive Test: Search for keywords you know exist in your content (e.g., a specific phrase from a Markdown file, a title, a component name). Verify that accurate search results appear.
- Negative Test: Search for a keyword you know does not exist. Verify that “No results found” or similar message is displayed.
- Empty Search: Type nothing, or delete your input. Verify the search results clear.
- Navigation: Click on a search result. Verify that it navigates you to the correct page and, if
highlightQueryis enabled, that the search term is highlighted on the page. - Configuration Change: Temporarily set
pagefind_enabled = falseinconfig.toml, rebuild, and verify that the search input and Pagefind assets are no longer present on the site. This confirms our conditional rendering works.
- Open
This comprehensive testing ensures that Pagefind is correctly integrated into both the build process and the frontend, providing a robust search experience.
Summary & Next Steps
In this chapter, we successfully integrated Pagefind into our Rust static site generator. We configured our SSG to execute Pagefind as a post-build step, allowing it to crawl our generated HTML and create a client-side search index. We then modified our Tera templates to include Pagefind’s UI assets and initialize the search functionality, providing a fast and efficient search experience for our users.
This addition significantly enhances the usability and discoverability of content on our static site, moving us closer to a production-ready content platform. We’ve also considered production aspects like error handling, performance, and security.
In the next chapter, Chapter 15: Incremental Builds and Caching, we will tackle the challenge of optimizing build times for large sites. We’ll explore strategies for detecting changes and only rebuilding what’s necessary, along with implementing caching mechanisms to speed up the development workflow and deployment process.