import { normalizeBasePath } from "../core/base-path.ts"; import type { ResolvedConfig } from "../core/schema.ts"; import { API_CATALOG_PATH, hasApiCatalog } from "./api-catalog.ts"; /** * The homepage `Link` response header (RFC 8288) — agent discovery for the * machine-readable surface Blume already publishes. Agents probing a site read * this header off `GET /` to find the resources without scraping HTML: * `agent-readability.json` and `llms.txt` as `rel="describedby"`, and the * homepage's raw-Markdown mirror as `rel="alternate"` (only when the home * route is a content page — a user landing page has no mirror). Both rel * values are IANA-registered, which agent-readiness checkers require. * * The header is homepage-only by design: the root response is what agents * probe, and `agent-readability.json` indexes the rest of the surface (the * per-route Markdown pattern, MCP, feeds) far better than per-page headers * could. An agent that enters on a deep page (a search result, a shared link) * never sees this header at all — that path is covered in the HTML instead: * every page's `` carries the same `describedby` links plus its own * Markdown mirror as an `alternate` (see `RootLayout.astro`), which also * reaches hosts where Blume can't set response headers. Targets are * root-relative under `deployment.base` — RFC 8288 resolves them against the * request URL. Returns null when nothing is advertisable. */ export const buildHomeLinkHeader = ( config: ResolvedConfig, routePaths: readonly string[] ): string | null => { const deployBase = normalizeBasePath(config.deployment.base); const links: string[] = []; // RFC 9727 §3: the api-catalog relation is how a homepage advertises the // well-known catalog. if (hasApiCatalog(config)) { links.push( `<${deployBase}${API_CATALOG_PATH}>; rel="api-catalog"; type="application/linkset+json"` ); } if (config.seo.agentReadability) { links.push( `<${deployBase}/agent-readability.json>; rel="describedby"; type="application/json"` ); } if (config.ai.llmsTxt.enabled) { links.push( `<${deployBase}/llms.txt>; rel="describedby"; type="text/plain"` ); } // Same route list as the negotiation surfaces (`markdownRoutePaths`): "/" // always has a mirror — the page's own source when the home route is a // content page, the synthesized llms.txt fallback otherwise — so callers // passing that list always advertise `/index.md` here. if (routePaths.includes("/")) { links.push( `<${deployBase}/index.md>; rel="alternate"; type="text/markdown"` ); } return links.length > 0 ? links.join(", ") : null; };