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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,6 +3,7 @@ node_modules/
|
||||
|
||||
# Build output
|
||||
_site/
|
||||
_pagefind/
|
||||
css/style.css
|
||||
|
||||
# Cache
|
||||
|
||||
1
404.njk
1
404.njk
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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: ".",
|
||||
|
||||
@@ -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
39
search.njk
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user