How to Integrate Orama with Astro: Complete Guide
Step-by-step guide to integrating Orama with your Astro website.
Orama is a full-text search engine that runs entirely in the browser. No server, no external API, no third-party service. Your search index is a JSON file that ships with your site, and all search queries execute client-side in JavaScript. For static Astro sites, this means you get instant search with zero infrastructure costs and zero API latency.
Orama handles typo tolerance, stemming, faceted search, and even vector search, all in a library that is under 10KB gzipped. It is particularly well-suited for documentation sites, blogs, and knowledge bases where the content is known at build time.
Prerequisites
- Node.js 18+
- An Astro project (
npm create astro@latest) - No external services needed
Installation
npm install @orama/orama
For additional features like stemming and language support:
npm install @orama/plugin-match-highlight @orama/stemmers
Creating the Search Index at Build Time
The key to using Orama with Astro is generating the search index during the build process. Create a script that reads your content and produces a JSON index:
// scripts/build-search-index.ts
import { create, insert, save } from "@orama/orama";
import fs from "fs";
import path from "path";
import matter from "gray-matter";
const postsDir = path.join(process.cwd(), "public/data/posts");
const files = fs.readdirSync(postsDir).filter((f) => f.endsWith(".mdx"));
const db = await create({
schema: {
title: "string",
description: "string",
content: "string",
slug: "string",
category: "string",
tags: "string",
author: "string",
},
});
for (const file of files) {
const raw = fs.readFileSync(path.join(postsDir, file), "utf-8");
const { data, content } = matter(raw);
if (data.draft) continue;
// Strip MDX/markdown syntax for cleaner search content
const plainContent = content
.replace(/```[\s\S]*?```/g, "") // Remove code blocks
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // Remove links, keep text
.replace(/#{1,6}\s/g, "") // Remove headings
.replace(/[*_~`]/g, "") // Remove formatting
.slice(0, 5000);
await insert(db, {
title: data.title || "",
description: data.description || "",
content: plainContent,
slug: file.replace(".mdx", ""),
category: data.category || "",
tags: (data.tags || []).join(", "),
author: data.author || "",
});
}
const serialized = await save(db);
const outputPath = path.join(process.cwd(), "public", "search-index.json");
fs.writeFileSync(outputPath, JSON.stringify(serialized));
console.log("Search index built with " + files.length + " posts");
Add it to your build process:
{
"scripts": {
"build:search": "npx tsx scripts/build-search-index.ts",
"build": "npm run build:search && astro build"
}
}
Building the Search Component
Create an Astro component that loads the index and runs searches client-side:
---
// src/components/OramaSearch.astro
---
<div id="orama-search">
<input
type="text"
id="search-input"
placeholder="Search articles..."
autocomplete="off"
/>
<ul id="search-results"></ul>
</div>
<script>
import { create, load, search } from "@orama/orama";
let db: any = null;
async function initSearch() {
if (db) return;
const response = await fetch("/search-index.json");
const data = await response.json();
db = await create({
schema: {
title: "string",
description: "string",
content: "string",
slug: "string",
category: "string",
tags: "string",
author: "string",
},
});
await load(db, data);
}
const input = document.getElementById("search-input") as HTMLInputElement;
const resultsContainer = document.getElementById("search-results") as HTMLUListElement;
let debounceTimer: ReturnType<typeof setTimeout>;
input.addEventListener("focus", () => {
initSearch(); // Load index on first focus
});
input.addEventListener("input", () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
await initSearch();
const query = input.value.trim();
if (!query) {
resultsContainer.replaceChildren();
return;
}
const results = await search(db, {
term: query,
properties: ["title", "description", "content"],
boost: { title: 3, description: 2, content: 1 },
limit: 10,
tolerance: 1,
});
resultsContainer.replaceChildren();
results.hits.forEach((hit: any) => {
const li = document.createElement("li");
li.className = "search-hit";
const link = document.createElement("a");
link.href = "/blog/" + hit.document.slug;
const title = document.createElement("h3");
title.textContent = hit.document.title;
const desc = document.createElement("p");
desc.textContent = hit.document.description;
link.appendChild(title);
link.appendChild(desc);
li.appendChild(link);
resultsContainer.appendChild(li);
});
}, 150);
});
</script>
<style>
#orama-search {
position: relative;
max-width: 600px;
}
#search-input {
width: 100%;
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 16px;
}
.search-hit {
list-style: none;
padding: 12px 16px;
border-bottom: 1px solid #eee;
}
.search-hit:hover {
background: #f5f5f5;
}
.search-hit a {
text-decoration: none;
color: inherit;
}
.search-hit h3 {
margin: 0 0 4px;
font-size: 16px;
}
.search-hit p {
margin: 0;
font-size: 14px;
color: #666;
}
</style>
Adding Search Highlighting
Use the highlight plugin to show which parts of the text matched:
import { create, load, search } from "@orama/orama";
import { afterInsert } from "@orama/plugin-match-highlight";
const db = await create({
schema: {
title: "string",
description: "string",
content: "string",
slug: "string",
},
plugins: [
{
name: "highlight",
afterInsert,
},
],
});
The plugin adds highlight positions to search results, which you can use to wrap matched terms in highlight elements.
Faceted Search
Orama supports faceted search for filtering by category, tags, or any other field:
const results = await search(db, {
term: "astro",
properties: ["title", "description"],
facets: {
category: {
limit: 10,
order: "DESC",
},
tags: {
limit: 20,
order: "DESC",
},
},
});
// results.facets.category.values gives you:
// { "Tutorials": 15, "Guides": 8, "News": 3 }
Use the facet counts to build filter buttons that refine search results.
Lazy Loading the Index
The search index can get large for sites with thousands of posts. Lazy load it so it does not affect initial page load:
// Only load when the user actually searches
let indexPromise: Promise<void> | null = null;
function ensureIndex() {
if (!indexPromise) {
indexPromise = fetch("/search-index.json")
.then((res) => res.json())
.then(async (data) => {
await load(db, data);
});
}
return indexPromise;
}
For very large sites, consider splitting the index into chunks by category or date range, and loading only the relevant chunk.
Production Tips
Strip unnecessary content from the index. Code blocks, HTML tags, and image references add bulk without improving search quality. Clean your content before indexing.
Set appropriate tolerance. Typo tolerance of 1 catches most typos without returning irrelevant results. A tolerance of 2 is more forgiving but may reduce precision.
Use boost weights. Weight title matches higher than content matches. A title match for "Astro deployment" is more relevant than the word appearing once in a 3,000-word article.
Compress the index. Gzip your
search-index.jsonon the server. Most hosting providers handle this automatically, but verify that the file is served compressed.Rebuild on content changes. Add the index build to your CI/CD pipeline so the search index updates whenever you publish new content.
Alternatives to Consider
- Pagefind if you want a similar build-time approach with automatic indexing and no manual schema definition.
- Meilisearch if you need server-side search with more advanced features like geo-search and scoped API keys.
- Algolia if you need analytics, A/B testing, and managed infrastructure.
Wrapping Up
Orama gives Astro sites powerful search with no infrastructure dependencies. The entire search engine runs in the browser, which means zero API calls, zero latency from network requests, and zero ongoing costs. For blogs, documentation sites, and any Astro project where content is known at build time, Orama is the simplest path to high-quality search. Build the index, ship the JSON, and search works everywhere your site loads.
Related Articles
How to Use Algolia with Astro: Complete Guide
Step-by-step guide to integrating Algolia with your Astro website.
How to Integrate Auth0 with Astro: Complete Guide
Step-by-step guide to integrating Auth0 with your Astro website. Setup, configuration, and best practices.
How to Use AWS Amplify with Astro: Complete Guide
Step-by-step guide to integrating AWS Amplify with your Astro website.