How-to Guide

How to Add Pagefind to a Static Site

Goal: Step-by-step guide to integrate Pagefind search functionality into your static site generator.

This guide shows you how to integrate Pagefind search functionality into your static site generator. If you want to add client-side search without a backend, Pagefind provides an efficient solution that indexes your HTML files and creates a searchable interface.

Prerequisites

  • A static site generator that outputs HTML files
  • Node.js and npm installed
  • Basic knowledge of HTML and JavaScript

Install Pagefind

Add Pagefind to your project dependencies:

bash
npm install pagefind @pagefind/default-ui

Configure Pagefind

Create a pagefind.yml configuration file in your project root:

yaml
site: build
source: build
output_subdir: pagefind
glob: "**/*.{html}"
root_selector: "html"
exclude_selectors:
    - nav[data-pagefind-ignore]
    - .main-footer
    - .theme-toggle
    - script
    - style
    - "[data-pagefind-ignore]"

# Custom index settings
ranking:
    page_rank_weight: 1.0
    term_weight: 1.0
    term_saturation: 2.0
    length_weight: 1.0

# Metadata extraction
merge_filter:
    type: "all"
    tags: "all"
    date: "all"

# Excerpt configuration
excerpt_length: 30
highlight_param: "mark"

Adjust the site and source paths to match your build output directory.

Add Pagefind to Your Build Process

Integrate Pagefind indexing into your build script. Add this function to your build process:

typescript
async function runPagefind(buildDir: string) {
	try {
		const { spawn } = await import("child_process");

		await new Promise<void>((resolve, reject) => {
			const pagefind = spawn("npx", [
				"pagefind",
				"--site", buildDir,
				"--output-subdir", "pagefind",
				"--exclude-selectors", "nav[data-pagefind-ignore],.main-footer,.theme-toggle,script,style,[data-pagefind-ignore]"
			], {
				stdio: "inherit",
			});

			pagefind.on("close", (code) => {
				if (code === 0) {
					resolve();
				} else {
					reject(new Error(`Pagefind exited with code ${code}`));
				}
			});

			pagefind.on("error", (error) => {
				reject(error);
			});
		});
	} catch (error) {
		console.warn("⚠️  Pagefind indexing failed:", error);
		console.warn("Search functionality may not work correctly.");
	}
}

Call this function after your static site generation completes.

Mark Content for Indexing

Add Pagefind data attributes to your HTML templates:

html
<!-- Main content area -->
<main class="main-content" data-pagefind-body
      data-pagefind-meta="type:article"
      data-pagefind-filter="tags:tutorial,javascript"
      data-pagefind-meta="date:2025-07-12">
  <!-- Your content here -->
</main>

<!-- Elements to exclude from search -->
<nav class="main-nav" data-pagefind-ignore>
  <!-- Navigation content -->
</nav>

Use these attributes:

  • data-pagefind-body: Marks the main content area for indexing
  • data-pagefind-meta: Adds metadata for filtering and display
  • data-pagefind-filter: Creates filterable categories
  • data-pagefind-ignore: Excludes elements from indexing

Create the Search Interface

Add the Pagefind script and create a search modal in your HTML template:

html
<head>
  <!-- Other head content -->
  <!-- <script src="/pagefind/pagefind.js"></script> -->
</head>

<body>
  <!-- Search button -->
  <button type="button" class="search-button" popovertarget="search-modal">
    Search
  </button>

  <!-- Search modal -->
  <div id="search-modal" popover="auto" class="search-modal">
    <div class="search-modal-header">
      <input type="text" id="search-input" class="search-input"
             placeholder="Search..." autocomplete="off">
      <button type="button" class="search-close"
              popovertarget="search-modal" popovertargetaction="hide">
        ×
      </button>
    </div>
    <div id="search-results" class="search-results">
      <div class="search-empty">Start typing to search...</div>
    </div>
  </div>
</body>

Implement Search Functionality

Add JavaScript to handle search interactions:

javascript
let pagefind;

window.addEventListener('DOMContentLoaded', async () => {
  // Initialize Pagefind
  pagefind = await import('/pagefind/pagefind.js');

  const searchInput = document.getElementById('search-input');
  const searchResults = document.getElementById('search-results');
  const searchModal = document.getElementById('search-modal');

  let searchTimeout;

  // Handle search input with debouncing
  searchInput.addEventListener('input', (e) => {
    clearTimeout(searchTimeout);
    const query = e.target.value.trim();

    if (!query) {
      searchResults.innerHTML = '<div class="search-empty">Start typing to search...</div>';
      return;
    }

    searchTimeout = setTimeout(() => performSearch(query), 200);
  });

  // Focus search input when modal opens
  searchModal.addEventListener('toggle', (e) => {
    if (e.newState === 'open') {
      setTimeout(() => searchInput.focus(), 100);
    }
  });

  // Clear search when modal closes
  searchModal.addEventListener('toggle', (e) => {
    if (e.newState === 'closed') {
      searchInput.value = '';
      searchResults.innerHTML = '<div class="search-empty">Start typing to search...</div>';
    }
  });

  async function performSearch(query) {
    try {
      searchResults.innerHTML = '<div class="search-loading">Searching...</div>';

      const search = await pagefind.search(query);

      if (search.results.length === 0) {
        searchResults.innerHTML = '<div class="search-empty">No results found</div>';
        return;
      }

      const resultsHtml = await Promise.all(
        search.results.slice(0, 8).map(async (result) => {
          const data = await result.data();
          return `
            <a href="${data.url}" class="search-result">
              <div class="search-result-title">${data.meta.title || 'Untitled'}</div>
              <div class="search-result-excerpt">${data.excerpt}</div>
              ${data.meta.type ? `<div class="search-result-type">${data.meta.type}</div>` : ''}
            </a>
          `;
        })
      );

      searchResults.innerHTML = resultsHtml.join('');
    } catch (error) {
      console.error('Search error:', error);
      searchResults.innerHTML = '<div class="search-error">Search failed</div>';
    }
  }
});

Style the Search Interface

Add CSS to style your search modal and results:

css
.search-modal {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 90%;
  max-width: 600px;
  max-height: 80vh;
  background: white;
  border: 1px solid #ccc;
  border-radius: 8px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
  z-index: 1000;
}

.search-input {
  width: 100%;
  padding: 12px;
  border: none;
  border-bottom: 1px solid #eee;
  font-size: 16px;
  outline: none;
}

.search-results {
  max-height: 400px;
  overflow-y: auto;
  padding: 8px;
}

.search-result {
  display: block;
  padding: 12px;
  border-radius: 4px;
  text-decoration: none;
  color: inherit;
  border-bottom: 1px solid #f0f0f0;
}

.search-result:hover {
  background-color: #f8f9fa;
}

.search-result-title {
  font-weight: 600;
  margin-bottom: 4px;
}

.search-result-excerpt {
  font-size: 14px;
  color: #666;
  line-height: 1.4;
}

.search-result-type {
  font-size: 12px;
  color: #888;
  margin-top: 4px;
}

Test Your Implementation

  1. Build your static site to generate HTML files
  2. Run the Pagefind indexing process
  3. Serve your site locally and test the search functionality
  4. Verify that search results appear correctly and link to the right pages

Troubleshooting

If search isn't working:

  • Check that the Pagefind script loads without errors
  • Verify that data-pagefind-body attributes are present on your content
  • Ensure the Pagefind index files are generated in the correct directory
  • Check browser console for JavaScript errors

Your static site now has client-side search functionality that works without a backend server.