/** * Custom Pagefind integration that extends Starlight's search index * with resource records (agents, skills, instructions, hooks, workflows, plugins). * * Starlight's pagefind is enabled (pagefind: true) so the search UI renders, * but this integration runs AFTER Starlight's and overwrites the index with * HTML pages + custom resource records combined. */ import type { AstroIntegration } from "astro"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import * as pagefind from "pagefind"; interface SearchRecord { type: string; id: string; title: string; description: string; path: string; tags?: string[]; searchText: string; } const TYPE_LABELS: Record = { agent: "Agent", instruction: "Instruction", skill: "Skill", hook: "Hook", workflow: "Workflow", plugin: "Plugin", tool: "Tool", }; const TYPE_PAGES: Record = { agent: "/agents/", instruction: "/instructions/", skill: "/skills/", hook: "/hooks/", workflow: "/workflows/", plugin: "/plugins/", tool: "/tools/", }; // Resource types that have a dedicated detail page at ///. Search // results for these should deep-link to the canonical detail page. const DETAIL_ROUTE_TYPES = new Set([ "agent", "instruction", "skill", "hook", "workflow", "plugin", ]); export default function pagefindResources(): AstroIntegration { let siteBase = "/"; return { name: "pagefind-resources", hooks: { "astro:config:done": ({ config }) => { siteBase = config.base; }, "astro:build:done": async ({ dir, logger }) => { const log = logger.fork("pagefind-resources"); const now = performance.now(); try { log.info("Building search index with Pagefind + resource records..."); const response = await pagefind.createIndex(); if (response.errors.length > 0) { for (const err of response.errors) log.error(err); throw new Error("Failed to create Pagefind index"); } const { index } = response; if (!index) { throw new Error("Pagefind index is undefined"); } // Index all built HTML pages (same as Starlight's default) const indexResult = await index.addDirectory({ path: fileURLToPath(dir), }); if (indexResult.errors.length > 0) { for (const err of indexResult.errors) log.error(err); throw new Error("Failed to index HTML directory"); } log.info(`Indexed ${indexResult.page_count} HTML pages.`); // Read and index resource records from search-index.json const searchIndexPath = fileURLToPath( new URL("./data/search-index.json", dir) ); let records: SearchRecord[]; try { records = JSON.parse(readFileSync(searchIndexPath, "utf-8")); } catch { log.warn( "Could not read search-index.json, skipping resource indexing." ); records = []; } // Use the base path from Astro config (e.g. "/") const base = siteBase.endsWith("/") ? siteBase : `${siteBase}/`; let added = 0; for (const record of records) { const hasDetailPage = DETAIL_ROUTE_TYPES.has(record.type) && Boolean(record.id); const typePage = TYPE_PAGES[record.type]; // Skip records we can neither deep-link nor point at a listing page. if (!hasDetailPage && !typePage) continue; const url = hasDetailPage ? `${base}${record.type}/${encodeURIComponent(record.id)}/` : `${base}${typePage.slice(1)}`; const typeLabel = TYPE_LABELS[record.type] || record.type; const addResult = await index.addCustomRecord({ url, content: record.searchText || `${record.title} ${record.description}`, language: "en", meta: { title: `${record.title} — ${typeLabel}`, }, filters: { type: [record.type], }, }); if (addResult.errors.length > 0) { for (const err of addResult.errors) log.warn(`Record ${record.id}: ${err}`); } else { added++; } } log.info(`Added ${added} resource records.`); // Write the combined index const writeResult = await index.writeFiles({ outputPath: fileURLToPath(new URL("./pagefind/", dir)), }); if (writeResult.errors.length > 0) { for (const err of writeResult.errors) log.error(err); throw new Error("Failed to write Pagefind files"); } const elapsed = performance.now() - now; log.info( `Search index built in ${ elapsed < 750 ? `${Math.round(elapsed)}ms` : `${(elapsed / 1000).toFixed(2)}s` }.` ); } catch (cause) { throw new Error("Failed to build Pagefind search index.", { cause }); } finally { await pagefind.close(); } }, }, }; }