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
Section titled “Getting Started”To run the development server locally, execute the following commands:
npm installnpm run devOnce the server is running, navigate to http://localhost:4321 in your browser to view the application.
Deployment
Section titled “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 serverIntegration Guide: staticseek with Astro
Section titled “Integration Guide: staticseek with Astro”1. Creating the Search Index
Section titled “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
Section titled “Key Configuration Points”- Use
GPULinearIndexfor search functionality (other index types are available). - Posts are retrieved using Astro’s
getCollectionfunction from the content collection. - The collection directory and schema are defined in
src/content.config.ts. - Configure
key_fieldsto specify which fields are available in search results (title and slug in this example). - Use
search_targetsto define which fields are searchable (title and content in this example). - The index is converted to JSON using
indexToObjectbefore being returned.
2. Implementing the Search Interface
Section titled “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
Section titled “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_fngenerated bycreateSearchFn. - The callback function set in the arguments of
createSearchFntoggles theis-loadingCSS class of the<div class="result"/>element. Due to the CSS selector settings defined at the end of this file, thedisplayproperty 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[*].wordaroundwith rangerefs[*].keyword_range.