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:
npm installnpm 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:
npm installnpm 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
:
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)));}
import { defineCollection, z } from "astro:content";import { glob } from "astro/loaders";
const posts = defineCollection({ loader: glob({ pattern: "./[^_]*.md", base: "posts" }), schema: z.object({ title: z.string(), }),});
export const collections = { posts };
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
):
---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 bycreateSearchFn
. - The callback function set in the arguments of
createSearchFn
toggles theis-loading
CSS class of the<div class="result"/>
element. Due to the CSS selector settings defined at the end of this file, thedisplay
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 rangerefs[*].keyword_range
.