{result.description}
))}{description}
{tags.map(tag => ( ))} ``` Now, searching for "Astro" in the intro paragraph ranks higher than "Astro" in the footer. To learn more, checkout the [weighting](https://pagefind.app/docs/weighting/) and [metadata](https://pagefind.app/docs/metadata/) docs. ## 3. The Search UI Since `pagefind.js` is a generated asset, we can't `import` it normally. We must use a dynamic import pointing to the URL. ```typescript export default function SearchModal() { const [pagefind, setPagefind] = useState(null); const [results, setResults] = useState([]); // Lazy load on open useEffect(() => { async function load() { // 1. Dynamic import from the public URL const lib = await import(/* @vite-ignore */ '/pagefind/pagefind.js'); await lib.init(); setPagefind(lib); } load(); }, []); const handleSearch = async (query) => { if (!pagefind || !query) return; // 2. Run Search const search = await pagefind.search(query); // 3. Load data for top 5 results // Pagefind returns "pointers" first, then we fetch the data const topResults = await Promise.all( search.results.slice(0, 5).map((r) => r.data()), ); setResults(topResults); }; // ... Render UI } ``` ## Where Pagefind Shines (and Fails) ### Where it is Extremely Effective 1. **Long-Form Documentation:** Pagefind supports **Sub-Result Anchors**. If you search "Permissions", and you have a massive guide called "Linux Basics", Pagefind can return a direct link to the `## Permissions` H2 inside that page (`/linux-guide#permissions`). Orama struggles with this without massive index bloat. _Enable it with `await pagefind.init({ showSubResults: true })`._ 2. **Multi-Site Search:** If you have a Blog (Astro) and a Documentation site (Starlight), Pagefind can **merge indexes**. You can run one search bar that queries both `blog.com/pagefind` and `docs.com/pagefind` seamlessly in the browser. ### Where it Fails 1. **Typo Tolerance:** This is the biggest trade-off. Orama uses [Levenshtein distance](https://github.com/oramasearch/orama/blob/main/packages/orama/src/components/levenshtein.ts) to match, for example, "astor" to "astro." Pagefind **does not**. [Pagefind](https://pagefind.app/docs/multilingual/) is a _stemming_ engine. It knows "run" matches "running." But if you type "rnning", it finds nothing. It relies on prefix matching, so typing "prog" finds "programming", but "pgramming" finds zero. 2. **Dev Experience:** There is no way around it: having to run a build to update the search index is annoying. If you write a new post, you won't see it in search until you restart the server with a fresh build. ## Conclusion Pagefind is the "Adult" choice for static search. It isn't as flashy as Orama, and it doesn't do vector embeddings or any other AI magic. But it scales from 10 pages to 10,000 pages without changing a line of code. --- ## What's next? This is part of a series of posts on implementing search for static sites: - [The Right Way to Add Orama Search to Astro](/blog/orama-astro) -- simple, zero-config search for small to medium sites - Why I Switched from Orama to Pagefind (**_you are here_**) -- chunked index for better scalability - [Meilisearch is the Best Search You'll Never Need](/blog/meilisearch-astro) -- server-side search with advanced features - [Why I Didn't Use Google Programmable Search](/blog/google-custom-search-astro) -- the hidden costs and indexing delays that make it impractical - [I Tried 4 Search Engines So You Don't Have To](/blog/astro-search-comparison) -- comprehensive comparison from a small blog perspective All with practical examples from a real production blog. --- --- title: The Right Way to Add Orama Search to Astro description: Stop using the Orama Astro plugin. Here is how to build a type-safe, metadata-rich search engine that actually works in Dev Mode. type: blog url: https://sarthakmishra.com/blog/orama-astro date: 2026-01-12 tags: ['astro', 'search', 'orama', 'preact', 'typescript', 'ui-design'] author: Sarthak Mishra excerpt: The official Orama plugin is great for demos, but it breaks in dev mode and sucks at metadata. Here is how I built a production-grade, zero-config search engine using Astro Endpoints. --- # The Right Way to Add Orama Search to Astro I love static sites. They are fast, secure, and cheap to host. But **search** has always been the weak point. You usually have two options: 1. **External Services:** Algolia or similar self-hosted services like Meilisearch. 2. **Client-Side Libraries:** Orama, Pagefind, Lunr or Fuse. I was looking for something that runs **entirely in the browser**, supports **fuzzy matching**, handles **typos**, and doesn't cost me a dime. Enter [Orama](https://orama.com/) (formerly Lyra). It's an immutable, in-memory, full-text search engine written in TypeScript. It's fast. However, most tutorials (including the official docs) tell you to use the `@orama/plugin-astro`. **Do not do this.** The plugin has three major problems: 1. **No Dev Mode:** It only generates the index during `astro build`. If you run `pnpm dev`, your search bar 404s. 2. **Weak Metadata:** It scrapes your HTML output. If you want to display a "Thumbnail" or "Author" in your search results, you have to build awkward side-channel maps. 3. **No Control:** You can't easily boost specific fields (like `title` > `body`) without fighting the config. Here is how to implement Orama the _right_ way using Astro Endpoints. ## The Architecture Instead of scraping HTML, we generate the index directly from **Astro Content Collections**. 1. **Build Time:** We create a `.json.ts` endpoint that fetches all posts, builds an Orama DB, and serializes it to JSON. 2. **Client Time:** The browser fetches this JSON file (lazy-loaded) and hydrates a local Orama instance. This solves the Dev Mode problem immediately because Astro treats `.json.ts` files as live routes. ## 1. The Index Endpoint Create a file at `src/pages/search-index.json.ts`. This acts as our "Search API." Defining a strict schema is important. Orama performs best when it knows exactly what fields to expect. By specifying fields like `title`, `slug`, `date`, and `description`, you optimize the index for performance and ensure each search result contains everything needed to render its card. ```typescript export async function GET() { const posts = await getCollection('blog', ({ data }) => !data.draft); // 1. Create the DB instance const db = await create({ schema: { title: 'string', slug: 'string', description: 'string', date: 'string', // Store as string for easy display tags: 'string[]', }, }); // 2. Insert Data const records = posts.map((post) => ({ title: post.data.title, slug: `/blog/${post.slug}`, description: post.data.description, date: post.data.date.toISOString(), tags: post.data.tags, })); await insertMultiple(db, records); // 3. Serialize to JSON const index = await save(db); return new Response(JSON.stringify(index), { headers: { 'Content-Type': 'application/json', }, }); } ``` ## 2. The Search Component (Client-Side) On the client, we fetch this JSON and "hydrate" Orama. I use Preact here to save bundle size, but the logic is framework-agnostic. We use `load` to restore the database state. This is much faster than re-indexing data on the client. ```typescript // Define the same schema for type safety const SCHEMA = { title: 'string', slug: 'string', description: 'string', date: 'string', tags: 'string[]', } as const; export default function SearchModal() { const [db, setDb] = useState${message}
|
|