The site is having more and more of blog posts now. And sometimes, when I need to find a specific post, it takes time. So a search function is much needed. After some quick research, I found a library called Pagefind. It is a search engine that can be integrated with any static site generator. In this post, I will walk you through the basic way to integrate it so that you can have something like this:
Why Pagefind?
There are a few reasons why I decided to go with Pagefind:
- Static site compatibility: Pagefind is designed specifically for static websites, making it an excellent choice for JAMstack and other static site architectures.
- Lightweight: The library is very lightweight, adding minimal overhead to your site.
- Easy setup: Pagefind can be implemented with minimal configuration, often requiring just a single line of JavaScript.
- No external dependencies: It doesn’t rely on external services or APIs, which can improve reliability and reduce costs.
- Automatic index generation: Pagefind automatically generates a search index from your static site content.
- Fast performance: The library offers quick search results, even on large sites. You can check out their demo on MDN Web Docs, very impressive.
- Multilingual support: Pagefind can handle content in multiple languages.
Installation
So in this post, I will show you how to integrate Pagefind with an Astro site, which uses Vite, so the process would be similar to other Vite-based sites.
First, you need to install the library itself and the plugin for Vite. You can do it by running the following command:
npm install --save-dev pagefind vite-plugin-pagefind
Configuration
Vite/Astro
In your Vite/Astro configuration file, you need to add the configuration for the plugin:
import pagefind from 'vite-plugin-pagefind'
export default defineConfig({ vite: { ssr: { noExternal: [ `${siteConfig.site}/pagefind/pagefind.js`, `${siteConfig.site}/pagefind/pagefind-highlight.js`, ], }, plugins: [pagefind()], build: { rollupOptions: { external: [ `${siteConfig.site}/pagefind/pagefind.js`, `${siteConfig.site}/pagefind/pagefind-highlight.js`, ], }, }, }, // ... other configurations})
Then you can create a pagefind.json file at the root of your project with the configuration for the plugin. I leave most of the configurations as default:
{ "site": "dist", "vite_plugin_pagefind": {}}
Indexing
In order for Pagefind to work, it needs to index your site. Let’s say you have your build output in the dist
folder. You can run the following command to index it:
pagefind
But we want to run automatic after every build. So we need to add a script to our package.json file:
"scripts": { "prepare": "simple-git-hooks", "compress": "esno scripts/img-compress-staged.ts", "dev": "astro dev --host", "build": "astro build", "postbuild": "pagefind", "preview": "astro preview", "lint": "eslint .", "lint:fix": "eslint --fix", "release": "bumpp" },
Search UI
The way I implemented the search UI is a bit custom. It consists of a search button, the search command palette, and the store for state management.
Button
<script setup lang="ts">import { showSearch } from '@/stores/search'</script>
<template> <button @click="showSearch = true"> <span class="sr-only">search</span> <span class="i-ri-search-line" /> </button></template>
store
import { ref } from 'vue'
export const showSearch = ref(false)
Command palette
This is where you can customize the search UI. You can find the my implementation at SearchCommandPalette.vue
Usage
Now you can use the search button and the command palette site. Let’s say we add it to the header component:
3 collapsed lines
---import { getLinkTarget } from '@/utils/link'import siteConfig from '@/site-config'import GlitchingLogo from './GlitchingLogo.vue'import Search from './Search.vue'import CommandPalette from './SearchCommandPalette.vue'import ThemeToggle from './ThemeToggle.vue'43 collapsed lines
const navLinks = siteConfig.header.navLinks || []
const socialLinks = structuredClone(siteConfig.socialLinks).filter((link: Record<string, any>) => { if (link.header && typeof link.header === 'boolean') { return link } else if (link.header && typeof link.header === 'string') { link.icon = link.header.includes('i-') ? link.header : link.icon return link } else { return false }})---
<header class="header relative z-40"> <a class="absolute m-7 select-none outline-none lg:fixed" href="/" aria-label="Logo"> <GlitchingLogo client:idle class="h-8 w-8" /> </a> <nav class="nav"> <div class="spacer" /> <div class="right tracking-tight print:op0 items-center"> {navLinks.map((link) => ( <a aria-label={link.text} target={getLinkTarget(link.href)} nav-link href={link.href} > {link.text} </a> ))} {socialLinks.map((link) => ( <a aria-label={link.text} class={`${link.icon} lt-md:hidden`} nav-link target={getLinkTarget(link.href)} href={link.href} /> ))} <a target="_blank" href="/rss.xml" class="lt-md:hidden i-ri-rss-line nav-link" aria-label="RSS" /> <Search client:visible /> <ThemeToggle client:load /> </div> </nav></header><CommandPalette client:load />
37 collapsed lines
<style>.header h1 { margin-bottom: 0;}
.logo { position: absolute; top: 1.5rem; left: 1.5rem;}
.nav { padding: 2rem; width: 100%; display: grid; grid-template-columns: auto max-content; box-sizing: border-box;}
.nav > * { margin: auto;}
.nav img { margin-bottom: 0;}
.nav .right { display: grid; grid-gap: 1.2rem; grid-auto-flow: column;}
.nav .right > * { margin: auto;}</style>
Conclusion
I hope this post has been helpful in setting up a search functionality for your website. If you have any questions or suggestions, feel free to reach out. I’m always happy to help!