feat: add Pagefind client-side search

Add Pagefind indexing after each Eleventy build with a search page at
/search/. Indexes main content only (sidebars excluded), supports dark
mode theming and URL query parameters (?q=).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ricardo
2026-02-05 10:49:05 +01:00
parent a8b44329d8
commit d9c84cad80
8 changed files with 114 additions and 5 deletions

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@ node_modules/
# Build output
_site/
_pagefind/
css/style.css
# Cache

View File

@@ -2,6 +2,7 @@
layout: layouts/base.njk
title: Page Not Found
permalink: /404.html
pagefindIgnore: true
---
<div class="text-center py-12">
<h1 class="text-4xl sm:text-6xl font-bold text-surface-300 dark:text-surface-700 mb-4">404</h1>

View File

@@ -79,7 +79,7 @@
<link rel="{{ social.rel }}" href="{{ social.url }}">
{% endfor %}
</head>
<body>
<body{% if pagefindIgnore %} data-pagefind-ignore="all"{% endif %}>
<script>
// Apply theme immediately to prevent flash
(function() {
@@ -155,6 +155,11 @@
</div>
<a href="/interactions/">Interactions</a>
</nav>
<a href="/search/" aria-label="Search" title="Search" class="p-2 rounded-lg text-surface-600 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/>
</svg>
</a>
<button id="theme-toggle" type="button" class="theme-toggle" aria-label="Toggle dark mode" title="Toggle dark mode">
<svg class="sun-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="5"/>
@@ -214,6 +219,7 @@
</div>
</div>
<a href="/interactions/">Interactions</a>
<a href="/search/">Search</a>
{# Mobile theme toggle #}
<button type="button" class="mobile-theme-toggle" aria-label="Toggle dark mode">
<span class="theme-label">Theme</span>
@@ -230,13 +236,13 @@
</nav>
</header>
<main class="container py-8">
<main class="container py-8" data-pagefind-body>
{% if withSidebar %}
<div class="layout-with-sidebar">
<div class="main-content">
{{ content | safe }}
</div>
<aside class="sidebar">
<aside class="sidebar" data-pagefind-ignore>
{% include "components/sidebar.njk" %}
</aside>
</div>
@@ -245,7 +251,7 @@
<div class="main-content">
{{ content | safe }}
</div>
<aside class="sidebar blog-sidebar">
<aside class="sidebar blog-sidebar" data-pagefind-ignore>
{% include "components/blog-sidebar.njk" %}
</aside>
</div>

View File

@@ -450,6 +450,45 @@
}
}
/* Pagefind UI theme overrides */
@layer components {
.pagefind-ui-container {
--pagefind-ui-scale: 1;
--pagefind-ui-primary: #2563eb;
--pagefind-ui-text: #18181b;
--pagefind-ui-background: #ffffff;
--pagefind-ui-border: #e4e4e7;
--pagefind-ui-tag: #f4f4f5;
--pagefind-ui-border-width: 1px;
--pagefind-ui-border-radius: 8px;
--pagefind-ui-font: inherit;
}
.dark .pagefind-ui-container {
--pagefind-ui-primary: #60a5fa;
--pagefind-ui-text: #f4f4f5;
--pagefind-ui-background: #09090b;
--pagefind-ui-border: #3f3f46;
--pagefind-ui-tag: #27272a;
}
.pagefind-ui-container .pagefind-ui__search-input {
@apply bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100;
}
.pagefind-ui-container .pagefind-ui__result-link {
@apply text-primary-600 dark:text-primary-400 hover:underline;
}
.pagefind-ui-container .pagefind-ui__result-excerpt {
@apply text-surface-600 dark:text-surface-400;
}
.pagefind-ui-container .pagefind-ui__message {
@apply text-surface-600 dark:text-surface-400;
}
}
/* Mobile-specific improvements */
@layer utilities {
/* Ensure proper touch scrolling on overflow containers */

View File

@@ -6,6 +6,7 @@ import sitemap from "@quasibit/eleventy-plugin-sitemap";
import markdownIt from "markdown-it";
import { minify } from "html-minifier-terser";
import { createHash } from "crypto";
import { execFileSync } from "child_process";
import { readFileSync } from "fs";
import { resolve, dirname } from "path";
import { fileURLToPath } from "url";
@@ -26,11 +27,17 @@ export default function (eleventyConfig) {
eleventyConfig.ignores.add("node_modules");
eleventyConfig.ignores.add("node_modules/**");
// Ignore Pagefind output directory
eleventyConfig.ignores.add("_pagefind");
eleventyConfig.ignores.add("_pagefind/**");
// Configure watch targets to exclude output directory
eleventyConfig.watchIgnores.add("_site");
eleventyConfig.watchIgnores.add("_site/**");
eleventyConfig.watchIgnores.add("/app/data/site");
eleventyConfig.watchIgnores.add("/app/data/site/**");
eleventyConfig.watchIgnores.add("_pagefind");
eleventyConfig.watchIgnores.add("_pagefind/**");
// Configure markdown-it with linkify enabled (auto-convert URLs to links)
const md = markdownIt({
@@ -395,6 +402,20 @@ export default function (eleventyConfig) {
.slice(0, 5);
});
// Pagefind indexing after each build
eleventyConfig.on("eleventy.after", ({ dir, runMode }) => {
try {
console.log(`[pagefind] Indexing ${dir.output} (${runMode})...`);
execFileSync("npx", ["pagefind", "--site", dir.output, "--glob", "**/*.html"], {
stdio: "inherit",
timeout: 60000,
});
console.log("[pagefind] Indexing complete");
} catch (err) {
console.error("[pagefind] Indexing failed:", err.message);
}
});
return {
dir: {
input: ".",

View File

@@ -19,7 +19,8 @@
"@quasibit/eleventy-plugin-sitemap": "^2.2.0",
"@atproto/api": "^0.12.0",
"eleventy-plugin-embed-everything": "^1.21.0",
"rss-parser": "^3.13.0"
"rss-parser": "^3.13.0",
"pagefind": "^1.3.0"
},
"devDependencies": {
"tailwindcss": "^3.4.0",

39
search.njk Normal file
View File

@@ -0,0 +1,39 @@
---
layout: layouts/base.njk
title: Search
permalink: /search/
eleventyExcludeFromCollections: true
pagefindIgnore: true
---
<div class="page-header mb-6 sm:mb-8">
<h1 class="text-2xl sm:text-3xl font-bold text-surface-900 dark:text-surface-100 mb-2">Search</h1>
<p class="text-surface-600 dark:text-surface-400">Search across all posts, articles, notes, and pages.</p>
</div>
<link rel="stylesheet" href="/_pagefind/pagefind-ui.css">
<div id="search"></div>
<script src="/_pagefind/pagefind-ui.js"></script>
<script>
window.addEventListener("DOMContentLoaded", () => {
const ui = new PagefindUI({
element: "#search",
showSubResults: true,
showImages: false,
});
// Support ?q= query parameter
const params = new URLSearchParams(window.location.search);
const query = params.get("q");
if (query) {
ui.triggerSearch(query);
}
// Auto-focus the search input
const input = document.querySelector("#search input[type='text']");
if (input && !query) {
input.focus();
}
});
</script>

View File

@@ -3,6 +3,7 @@ layout: layouts/base.njk
title: Webmention Debug
permalink: /debug/webmentions/
eleventyExcludeFromCollections: true
pagefindIgnore: true
---
<div class="page-header mb-6 sm:mb-8">
<h1 class="text-2xl sm:text-3xl font-bold text-surface-900 dark:text-surface-100 mb-2">Webmention Debug</h1>