Every PKMS/BASB needs a search functionality. Ever since I’ve created brainfck to host my
own collection of thoughts/ideas/resources (aka Zettelkasten) I wanted to be able to
actually search within my collection of org-roam based notes. Meanwhile for all my sites I
own (this blog, my CV/portfolio, brainfck and defersec) I use hugo. All of them didn’t
have proper search capabilities. That’s why I was looking for a proper way to include
search functionalities without any major effort.
Hugo, indeed, has some open-source and commercial search options you can choose from. I
have used this fuse.js integration in the past but I wasn’t happy with it. It didn’t index
well, I couldn’t find all my content. Of course, I was thinking having Algolia DocSearch
do the magic but I one has to apply for it. Also, not all of my sites are about technical
documentation. So I had to find another alternative. Digging deeper I came across
Pagefind.
Here is a screenshot how it currently looks like:
Pagefind
Pagefind is a lightweight, static search solution designed specifically for static sites
like those built with Hugo. It works by generating a search index during your site’s build
process, creating a client-side search experience that doesn’t require any server
infrastructure.
The indexing works by crawling your static HTML files after they’re built, extracting
content and metadata. Pagefind creates a compressed search index that’s both fast and
efficient, typically resulting in index sizes of about 1/1000th of your original content
size. This means your search functionality remains quick even on larger sites.
When examining a typical Pagefind implementation in a Hugo site, the folder structure
looks something like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
tree public/pagefind
public/pagefind
├── fragment
│ ├── en_02aee83.pf_fragment
...
│ ├── en_2538bf9.pf_fragment
│ ├── en_2550e62.pf_fragment
│ ├── en_2582c6f.pf_fragment
│ ├── en_ffbbc35.pf_index
│ └── en_ffeab69.pf_index
├── pagefind-entry.json
├── pagefind-highlight.js
├── pagefind-modular-ui.css
├── pagefind-modular-ui.js
├── pagefind-ui.css
├── pagefind-ui.js
├── pagefind.en_19e6da436f.pf_meta
├── pagefind.en_1c7f38a66e.pf_meta
├── pagefind.en_1f95755b6e.pf_meta
├── pagefind.en_2a7661ff21.pf_meta
├── pagefind.en_4fdfe13af9.pf_meta
├── pagefind.en_5049ae833f.pf_meta
├── pagefind.en_603dab85b4.pf_meta
├── pagefind.en_6663d2c9c9.pf_meta
├── pagefind.en_7411b4b912.pf_meta
├── pagefind.en_791636c92e.pf_meta
├── pagefind.en_93f1fd4c5c.pf_meta
├── pagefind.en_94bcd0c843.pf_meta
├── pagefind.en_a8061c44f1.pf_meta
├── pagefind.en_a96223e65e.pf_meta
├── pagefind.en_b526d7e391.pf_meta
├── pagefind.en_b6e37679e1.pf_meta
├── pagefind.en_b7a63d2937.pf_meta
├── pagefind.en_c039de83fe.pf_meta
├── pagefind.en_d9c4de17e6.pf_meta
├── pagefind.en_deac3933f1.pf_meta
├── pagefind.en_eb3680ac82.pf_meta
├── pagefind.en_fab96f64ed.pf_meta
├── pagefind.js
├── wasm.en.pagefind
└── wasm.unknown.pagefind
3 directories, 3087 files
|
Code Snippet 1:
Contents of the pagefind folder
Configuration
You also have some configuration options. In my case:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
# Pagefind configuration
# Basic options (using Pagefind 1.0 naming)
site: public
output_subdir: pagefind
# Add date-based metadata for sorting
indexing_options:
# Add date as metadata for all pages
- metadata_date_field: date
metadata_date_format: iso
# Global metadata fields for all pages
- metadata_fields:
# Primary metadata sources
- {
tag: "meta[property='og:title']",
as: "title",
content_attr: "content",
optional: true,
}
- { selector: ".post-title", as: "title", optional: true }
- { selector: "h1", as: "title", optional: true }
# Date and other metadata
- {
tag: "meta[property='og:type']",
as: "type",
content_attr: "content",
optional: true,
}
- {
tag: "meta[name='date']",
as: "date",
content_attr: "content",
optional: true,
}
- {
tag: "meta[property='article:published_time']",
as: "published",
content_attr: "content",
optional: true,
}
- {
selector: "time",
as: "date",
content_attr: "datetime",
optional: true,
}
# Added languages
language:
code: en
stemming: true
# Search options
search_options:
ignore_missing_metadata_fields: true
boost_exact_matches: true
boost_title: 5.0
|
This configuration file sets up Pagefind with optimal settings for a personal knowledge
base. The key options include:
- Basic setup: Specifies the source directory (public) and where to output the search
files (pagefind subdirectory)
- Date-based metadata: Adds date fields for each page, enabling chronological sorting of
search results
- Metadata extraction: Configures multiple fallback methods to extract page titles and
dates from different HTML elements and meta tags
- Search optimization: Boosts exact matches and increases the weight of title matches by
5x, ensuring the most relevant results appear first
Hugo integration
search page
1
2
3
|
---
title: "Search"
---
|
search template
The search page implementation works through several key components:
- Loading Pagefind scripts: The template loads the necessary CSS and JavaScript files that
Pagefind generated during the build process.
- Search parameter handling: The implementation supports direct linking to search
results using URL parameters. When someone visits
/search?q=zettelkasten
, the page
automatically populates the search box with “zettelkasten” and triggers the search.
- URL updating: As the user types in the search box, the URL is dynamically updated with
the current query using the browser’s History API (pushState). This creates a seamless
experience where users can bookmark or share specific search results.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
|
{{ define "main" }}
<main class="center mv4 content-width ph3">
<div class="f3 fw6 heading-color heading-font">{{ .Title }}</div>
<div class="lh-copy mt4">
<p>Search across all content.</p>
<!-- Load Pagefind scripts -->
<link href="/pagefind/pagefind-ui.css" rel="stylesheet" />
<script src="/pagefind/pagefind-ui.js"></script>
<!-- Create the search container with your site's styling -->
<div id="search" class="w-100 mb4"></div>
<script>
// Set configuration for pagefind
window.pagefindConfig = {
bundlePath: "/pagefind/",
processTerm: (term) => {
// Simple processing to remove undefined text from search results
return term.replace(/undefined/g, "");
},
};
// Function to get URL parameters
function getUrlParameter(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
const regex = new RegExp("[\\?&]" + name + "=([^&#]*)");
const results = regex.exec(location.search);
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
// Initialize the search UI when the DOM is loaded
document.addEventListener("DOMContentLoaded", function () {
// Get the search query from URL parameter 'q' if it exists
const initialQuery = getUrlParameter("q");
const searchUI = new PagefindUI({
element: "#search",
showSubResults: true,
showImages: false,
sort: {
// Enable sorting by date (newest first) and relevance
options: [
{ key: "date", label: "Date (Newest First)", order: "desc" },
{ key: "default", label: "Relevance" },
],
// Use date as the default sorting method
default: "date",
},
translations: {
placeholder: "Search your notes...",
zero_results: "No results found",
many_results: "Found {results} results",
sort_by: "Sort by:",
},
});
// Set up a listener to watch for changes in the search input
setTimeout(() => {
// Find the search input after PagefindUI has initialized
const searchInput = document.querySelector(
".pagefind-ui__search-input",
);
const searchForm = document.querySelector(".pagefind-ui__form");
if (searchInput) {
// Update URL when the search input changes
searchInput.addEventListener("input", function () {
updateSearchUrl(this.value);
});
// Also capture form submission (when user presses Enter)
if (searchForm) {
searchForm.addEventListener("submit", function (e) {
// Don't prevent default as we want the search to execute
updateSearchUrl(searchInput.value);
});
}
}
// Helper function to update the URL with the search query
function updateSearchUrl(query) {
const url = new URL(window.location);
if (query && query.trim() !== "") {
url.searchParams.set("q", query);
} else {
url.searchParams.delete("q");
}
window.history.pushState({}, "", url);
}
}, 500); // Short delay to ensure PagefindUI has initialized
// If there's an initial query, set it and trigger the search
if (initialQuery) {
// Use a small timeout to ensure PagefindUI is fully initialized
setTimeout(() => {
searchUI.triggerSearch(initialQuery);
}, 100);
}
});
</script>
</div>
</main>
{{ end }}
|
Code Snippet 4:
The search template for Pagefind
CSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
/* Simple pagefind overrides to match site styling */
:root {
--pagefind-ui-scale: 1;
--pagefind-ui-primary: #3e5622;
--pagefind-ui-text: #333;
--pagefind-ui-background: #fff;
--pagefind-ui-border: #ddd;
--pagefind-ui-border-radius: 4px;
}
/* Search input styling */
.pagefind-ui__search-input {
padding: 0.5rem !important;
width: 100% !important;
border: 1px solid #ddd !important;
border-radius: 0.25rem !important;
font-size: 1rem !important;
/* Remove the search icon */
background-image: none !important;
padding-right: 16px !important;
padding-left: 16px !important;
}
/* Adjust clear button position */
.pagefind-ui__search-clear {
right: 16px !important;
}
/* Result title styling */
.pagefind-ui__result-title {
font-weight: 600 !important;
}
.pagefind-ui__result-link {
color: #3e5622 !important;
text-decoration: none !important;
}
/* Highlight search terms */
.pagefind-ui__result-excerpt mark {
background-color: rgba(255, 255, 0, 0.4) !important;
color: inherit !important;
}
/* Load more button styling */
.pagefind-ui__button {
background-color: #3e5622 !important;
color: white !important;
border: none !important;
border-radius: 0.25rem !important;
padding: 0.5rem 1rem !important;
cursor: pointer !important;
}
/* Simple fix for undefined text */
.pagefind-ui__result-excerpt:contains('undefined'),
.pagefind-ui__result-title:contains('undefined') {
visibility: hidden;
}
|
Netlify deployment
Deploying your Pagefind-enabled Hugo site on Netlify is straightforward with this configuration:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
[build]
publish = "public"
command = "hugo && npx pagefind --site public --output-subdir pagefind"
[build.environment]
HUGO_VERSION = "0.145.0" # Replace with your current Hugo version
NODE_VERSION = "18" # Ensure we have a recent Node version for Pagefind
# Cache control for Pagefind files
[[headers]]
for = "/pagefind/*"
[headers.values]
Cache-Control = "public, max-age=604800"
|
The configuration does three critical things:
- Build command: It extends the standard Hugo build process by adding Pagefind indexing as
a second step. This ensures your search index is generated after Hugo creates all the
HTML files.
- Environment setup: It specifies the required Hugo and Node.js versions.
- Cache configuration: It adds cache headers for all Pagefind files, setting a one-week
cache period. This improves performance for returning visitors, as browsers won’t need
to download the search index again on every visit.
For local development this might be also useful:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
{
"name": "brainfck-roam",
"version": "1.0.0",
"description": "Your org-roam notes on Hugo",
"scripts": {
"build": "hugo && npx pagefind --site public --output-subdir pagefind",
"dev": "hugo server -b http://127.0.0.1:1315/ --disableFastRender --port 1315 --noHTTPCache --logLevel debug --gc",
"search-index": "npx pagefind --site public --output-subdir pagefind",
"full-dev": "npm run build && npm run dev"
},
"dependencies": {
"pagefind": "^1.0.0"
}
}
|
Code Snippet 7:
package.json
The package.json
file provides several convenient npm scripts that streamline your workflow:
npm run dev
- Starts a local Hugo server with debug logging and cache disabled.
npm run search-index
- Runs only the Pagefind indexing on your current build output. This is helpful when you
want to rebuild just the search index without regenerating the entire site.
npm run build
- Performs the complete production build process—generating the Hugo site and then
creating the Pagefind search index afterward.
npm run full-dev
- A comprehensive development command that builds the complete site with search index
and then starts the development server. This is ideal when you need to test search
functionality locally.
Resources