Start typing to search...
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:
npm install pagefind @pagefind/default-ui
Configure Pagefind Create a pagefind.yml configuration file in your project root:
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:
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:
<!-- 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 indexingdata-pagefind-meta: Adds metadata for filtering and displaydata-pagefind-filter: Creates filterable categoriesdata-pagefind-ignore: Excludes elements from indexingCreate the Search Interface Add the Pagefind script and create a search modal in your HTML template:
< 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:
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:
.search-modal {
position : fixed ;
top : 50 % ;
left : 50 % ;
transform : translate ( -50 % , -50 % );
width : 90 % ;
max-width : 600 px ;
max-height : 80 vh ;
background : white ;
border : 1 px solid #ccc ;
border-radius : 8 px ;
box-shadow : 0 4 px 20 px rgba ( 0 , 0 , 0 , 0.15 );
z-index : 1000 ;
}
.search-input {
width : 100 % ;
padding : 12 px ;
border : none ;
border-bottom : 1 px solid #eee ;
font-size : 16 px ;
outline : none ;
}
.search-results {
max-height : 400 px ;
overflow-y : auto ;
padding : 8 px ;
}
.search-result {
display : block ;
padding : 12 px ;
border-radius : 4 px ;
text-decoration : none ;
color : inherit ;
border-bottom : 1 px solid #f0f0f0 ;
}
.search-result:hover {
background-color : #f8f9fa ;
}
.search-result-title {
font-weight : 600 ;
margin-bottom : 4 px ;
}
.search-result-excerpt {
font-size : 14 px ;
color : #666 ;
line-height : 1.4 ;
}
.search-result-type {
font-size : 12 px ;
color : #888 ;
margin-top : 4 px ;
}
Test Your Implementation Build your static site to generate HTML files Run the Pagefind indexing process Serve your site locally and test the search functionality 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.
-->\n\n\n\n \n \n Search\n \n\n \n \n \n
\n
Start typing to search...
\n
\n
\n\n","meta":"","title":"","highlighted":"< head > \n <!-- Other head content --> \n <!-- <script src=\"/pagefind/pagefind.js\"></script> --> \n</ head > \n \n< body > \n <!-- Search button --> \n < button type = \"button\" class = \"search-button\" popovertarget = \"search-modal\" > \n Search \n </ button > \n \n <!-- Search modal --> \n < div id = \"search-modal\" popover = \"auto\" class = \"search-modal\" > \n < div class = \"search-modal-header\" > \n < input type = \"text\" id = \"search-input\" class = \"search-input\" \n placeholder = \"Search...\" autocomplete = \"off\" > \n < button type = \"button\" class = \"search-close\" \n popovertarget = \"search-modal\" popovertargetaction = \"hide\" > \n × \n </ button > \n </ div > \n < div id = \"search-results\" class = \"search-results\" > \n < div class = \"search-empty\" >Start typing to search...</ div > \n </ div > \n </ div > \n</ body > \n "},"children":[]},{"$$mdtype":"Tag","name":"h2","attributes":{"id":"implement-search-functionality"},"children":["Implement Search Functionality"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Add JavaScript to handle search interactions:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"language":"javascript","content":"let pagefind;\n\nwindow.addEventListener('DOMContentLoaded', async () => {\n // Initialize Pagefind\n pagefind = await import('/pagefind/pagefind.js');\n\n const searchInput = document.getElementById('search-input');\n const searchResults = document.getElementById('search-results');\n const searchModal = document.getElementById('search-modal');\n\n let searchTimeout;\n\n // Handle search input with debouncing\n searchInput.addEventListener('input', (e) => {\n clearTimeout(searchTimeout);\n const query = e.target.value.trim();\n\n if (!query) {\n searchResults.innerHTML = 'Start typing to search...
';\n return;\n }\n\n searchTimeout = setTimeout(() => performSearch(query), 200);\n });\n\n // Focus search input when modal opens\n searchModal.addEventListener('toggle', (e) => {\n if (e.newState === 'open') {\n setTimeout(() => searchInput.focus(), 100);\n }\n });\n\n // Clear search when modal closes\n searchModal.addEventListener('toggle', (e) => {\n if (e.newState === 'closed') {\n searchInput.value = '';\n searchResults.innerHTML = 'Start typing to search...
';\n }\n });\n\n async function performSearch(query) {\n try {\n searchResults.innerHTML = 'Searching...
';\n\n const search = await pagefind.search(query);\n\n if (search.results.length === 0) {\n searchResults.innerHTML = 'No results found
';\n return;\n }\n\n const resultsHtml = await Promise.all(\n search.results.slice(0, 8).map(async (result) => {\n const data = await result.data();\n return `\n \n ${data.meta.title || 'Untitled'}
\n ${data.excerpt}
\n ${data.meta.type ? `${data.meta.type}
` : ''}\n \n `;\n })\n );\n\n searchResults.innerHTML = resultsHtml.join('');\n } catch (error) {\n console.error('Search error:', error);\n searchResults.innerHTML = 'Search failed
';\n }\n }\n});\n","meta":"","title":"","highlighted":"let pagefind; \n \nwindow. addEventListener ( 'DOMContentLoaded' , async () => { \n // Initialize Pagefind \n pagefind = await import ( '/pagefind/pagefind.js' ); \n \n const searchInput = document. getElementById ( 'search-input' ); \n const searchResults = document. getElementById ( 'search-results' ); \n const searchModal = document. getElementById ( 'search-modal' ); \n \n let searchTimeout; \n \n // Handle search input with debouncing \n searchInput. addEventListener ( 'input' , ( e ) => { \n clearTimeout (searchTimeout); \n const query = e.target.value. trim (); \n \n if ( ! query) { \n searchResults.innerHTML = '<div class=\"search-empty\">Start typing to search...</div>' ; \n return ; \n } \n \n searchTimeout = setTimeout (() => performSearch (query), 200 ); \n }); \n \n // Focus search input when modal opens \n searchModal. addEventListener ( 'toggle' , ( e ) => { \n if (e.newState === 'open' ) { \n setTimeout (() => searchInput. focus (), 100 ); \n } \n }); \n \n // Clear search when modal closes \n searchModal. addEventListener ( 'toggle' , ( e ) => { \n if (e.newState === 'closed' ) { \n searchInput.value = '' ; \n searchResults.innerHTML = '<div class=\"search-empty\">Start typing to search...</div>' ; \n } \n }); \n \n async function performSearch ( query ) { \n try { \n searchResults.innerHTML = '<div class=\"search-loading\">Searching...</div>' ; \n \n const search = await pagefind. search (query); \n \n if (search.results. length === 0 ) { \n searchResults.innerHTML = '<div class=\"search-empty\">No results found</div>' ; \n return ; \n } \n \n const resultsHtml = await Promise . all ( \n search.results. slice ( 0 , 8 ). map ( async ( result ) => { \n const data = await result. data (); \n return ` \n <a href=\"${ data . url }\" class=\"search-result\"> \n <div class=\"search-result-title\">${ data . meta . title || 'Untitled'}</div> \n <div class=\"search-result-excerpt\">${ data . excerpt }</div> \n ${ data . meta . type ? `<div class=\"search-result-type\">${ data . meta . type }</div>` : ''} \n </a> \n ` ; \n }) \n ); \n \n searchResults.innerHTML = resultsHtml. join ( '' ); \n } catch (error) { \n console. error ( 'Search error:' , error); \n searchResults.innerHTML = '<div class=\"search-error\">Search failed</div>' ; \n } \n } \n}); \n "},"children":[]},{"$$mdtype":"Tag","name":"h2","attributes":{"id":"style-the-search-interface"},"children":["Style the Search Interface"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Add CSS to style your search modal and results:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"language":"css","content":".search-modal {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 90%;\n max-width: 600px;\n max-height: 80vh;\n background: white;\n border: 1px solid #ccc;\n border-radius: 8px;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);\n z-index: 1000;\n}\n\n.search-input {\n width: 100%;\n padding: 12px;\n border: none;\n border-bottom: 1px solid #eee;\n font-size: 16px;\n outline: none;\n}\n\n.search-results {\n max-height: 400px;\n overflow-y: auto;\n padding: 8px;\n}\n\n.search-result {\n display: block;\n padding: 12px;\n border-radius: 4px;\n text-decoration: none;\n color: inherit;\n border-bottom: 1px solid #f0f0f0;\n}\n\n.search-result:hover {\n background-color: #f8f9fa;\n}\n\n.search-result-title {\n font-weight: 600;\n margin-bottom: 4px;\n}\n\n.search-result-excerpt {\n font-size: 14px;\n color: #666;\n line-height: 1.4;\n}\n\n.search-result-type {\n font-size: 12px;\n color: #888;\n margin-top: 4px;\n}\n","meta":"","title":"","highlighted":".search-modal { \n position : fixed ; \n top : 50 % ; \n left : 50 % ; \n transform : translate ( -50 % , -50 % ); \n width : 90 % ; \n max-width : 600 px ; \n max-height : 80 vh ; \n background : white ; \n border : 1 px solid #ccc ; \n border-radius : 8 px ; \n box-shadow : 0 4 px 20 px rgba ( 0 , 0 , 0 , 0.15 ); \n z-index : 1000 ; \n} \n \n.search-input { \n width : 100 % ; \n padding : 12 px ; \n border : none ; \n border-bottom : 1 px solid #eee ; \n font-size : 16 px ; \n outline : none ; \n} \n \n.search-results { \n max-height : 400 px ; \n overflow-y : auto ; \n padding : 8 px ; \n} \n \n.search-result { \n display : block ; \n padding : 12 px ; \n border-radius : 4 px ; \n text-decoration : none ; \n color : inherit ; \n border-bottom : 1 px solid #f0f0f0 ; \n} \n \n.search-result:hover { \n background-color : #f8f9fa ; \n} \n \n.search-result-title { \n font-weight : 600 ; \n margin-bottom : 4 px ; \n} \n \n.search-result-excerpt { \n font-size : 14 px ; \n color : #666 ; \n line-height : 1.4 ; \n} \n \n.search-result-type { \n font-size : 12 px ; \n color : #888 ; \n margin-top : 4 px ; \n} \n "},"children":[]},{"$$mdtype":"Tag","name":"h2","attributes":{"id":"test-your-implementation"},"children":["Test Your Implementation"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Build your static site to generate HTML files"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Run the Pagefind indexing process"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Serve your site locally and test the search functionality"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Verify that search results appear correctly and link to the right pages"]}]},{"$$mdtype":"Tag","name":"h2","attributes":{"id":"troubleshooting"},"children":["Troubleshooting"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If search isn't working:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Check that the Pagefind script loads without errors"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Verify that ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["data-pagefind-body"]}," attributes are present on your content"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Ensure the Pagefind index files are generated in the correct directory"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Check browser console for JavaScript errors"]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Your static site now has client-side search functionality that works without a backend server."]}]};
window.__FRONTMATTER__ = {"title":"How to Add Pagefind to a Static Site","description":"Step-by-step guide to integrate Pagefind search functionality into your static site generator.","type":"howto","date":"2025-07-12T00:00:00.000Z","tags":["pagefind","static-site","search"]};