Files
awesome-copilot/website/src/integrations/pagefind-resources.ts
T
Aaron Powell 1a77b83008 Remove website modal and use cookbook detail routes (#2312)
- replace cookbook recipe/example modal views with dedicated detail pages
- add legacy #file cookbook hash redirects to canonical routes
- remove modal component/runtime and associated CSS and stale page references

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: ca1a64ec-678f-45ac-ba68-33a37717398a
2026-07-16 15:17:05 +10:00

171 lines
5.2 KiB
TypeScript

/**
* 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<string, string> = {
agent: "Agent",
instruction: "Instruction",
skill: "Skill",
hook: "Hook",
workflow: "Workflow",
plugin: "Plugin",
tool: "Tool",
};
const TYPE_PAGES: Record<string, string> = {
agent: "/agents/",
instruction: "/instructions/",
skill: "/skills/",
hook: "/hooks/",
workflow: "/workflows/",
plugin: "/plugins/",
tool: "/tools/",
};
// Resource types that have a dedicated detail page at /<type>/<id>/. 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();
}
},
},
};
}