Skip to content

Prepare index at deployment and read index on-demand (Astro)

This guide demonstrates how to integrate staticseek into a Astro application. You can find the complete implementation in our GitHub repository and see it in action at our live demo.

Getting Started

To run the development server locally, execute the following commands:

Terminal window
npm install
npm run dev

Once the server is running, navigate to http://localhost:4321 in your browser to view the application.

Deployment

This example is optimized for static file deployment. To generate and deploy the static files, use the following commands:

Terminal window
npm install
npm run build
# Upload the generated "dist" directory to your HTTP server

Integration Guide: staticseek with Astro

1. Creating the Search Index

First, create a static index file. The following example demonstrates how to set this up at src/pages/searchindex.json.ts:

src/pages/searchindex.json.ts
import { getCollection } from "astro:content";
import { GPULinearIndex, StaticSeekError, createIndex, indexToObject } from "staticseek";
export async function GET() {
const posts = await getCollection("posts");
const linear_index = createIndex(GPULinearIndex, posts, {
search_targets: ["body", "data.title"],
key_fields: ["data.title", "id"],
});
if (linear_index instanceof StaticSeekError) {
return new Response(null, { status: 500, statusText: linear_index.message });
}
return new Response(JSON.stringify(indexToObject(linear_index)));
}

Key Configuration Points

  • Use GPULinearIndex for search functionality (other index types are available).
  • Posts are retrieved using Astro’s getCollection function from the content collection.
  • The collection directory and schema are defined in src/content.config.ts.
  • Configure key_fields to specify which fields are available in search results (title and slug in this example).
  • Use search_targets to define which fields are searchable (title and content in this example).
  • The index is converted to JSON using indexToObject before being returned.

2. Implementing the Search Interface

Create a search page (e.g., in src/pages/index.astro):

src/pages/index.astro
---
import Html from "../layout/Html.astro";
---
<Html>
<section>
<div class="input-area">
<div>Search</div>
<input type="text" name="search" id="input-text" placeholder="Type your search query in English..." />
</div>
<div class="loading">
Loading index...
</div>
<div class="result">
<h2>Results</h2>
<ul id="search-result"></ul>
</div>
</section>
</Html>
<script>
import { createSearchFn, StaticSeekError } from "staticseek";
import * as v from "valibot";
const schema = v.object({
id: v.string(),
data: v.object({
title: v.string(),
}),
});
const input_text = document.querySelector<HTMLInputElement>("#input-text");
const search_result = document.querySelector<HTMLUListElement>("#search-result");
if (search_result === null) throw new Error("Cannot find search-result.");
const search_fn = createSearchFn("/searchindex.json", (x) => {
if(x) {
search_result.classList.add("is-loading");
} else {
search_result.classList.remove("is-loading");
}
} );
input_text?.addEventListener("input", async () => {
const result = await search_fn(input_text.value);
search_result.innerText = "";
if (!(result instanceof StaticSeekError)) {
for (const item of result) {
const key = v.parse(schema, item.key);
const li = document.createElement("li");
const ref = item.refs[0];
if(ref.wordaround && ref.keyword_range) {
const wa_pre = ref.wordaround.slice(0, ref.keyword_range[0]);
const wa_kwd = ref.wordaround.slice(ref.keyword_range[0], ref.keyword_range[1]);
const wa_post = ref.wordaround.slice(ref.keyword_range[1]);
li.innerHTML = `<a href="/posts/${key.id}"><h3>${key.data.title}</h3></a><p>${wa_pre}<em>${wa_kwd}</em>${wa_post}</p>`;
} else {
li.innerHTML = `<a href="/posts/${key.id}"><h3>${key.data.title}</h3></a>`;
}
search_result.appendChild(li);
}
}
});
</script>
<style>
.result:has(#search-result.is-loading) {
display: none;
}
.loading {
display: none;
}
section:has(#search-result.is-loading) > .loading {
display: block;
}
</style>

Implementation Details

For simplicity, this example uses innerHTML. However, in production applications, you should use createElement and appendChild methods instead of innerHTML to mitigate security risks associated with direct HTML injection.

While staticseek operates on the client side, code written in the Astro component’s script section only executes once during deployment. Therefore, staticseek is implemented within a script element in the component template.

  • The search index is loaded only once during the first call of search_fn generated by createSearchFn.
  • The callback function set in the arguments of createSearchFn toggles the is-loading CSS class of the <div class="result"/> element. Due to the CSS selector settings defined at the end of this file, the display property changes, toggling the loading indicator.
  • Search results are sorted by relevance score.
  • Each result includes:
    • The key fields specified during index creation (title and slug).
    • The key is parsed using valibot.
    • A link to the full post using the id and title of the key fields.
    • Matched content context via refs[*].wordaround with range refs[*].keyword_range.