import { readFileSync } from "node:fs"; import { join } from "node:path"; import type { Hono } from "hono"; import { publicRepoNames } from "../server/db.ts"; import type { PageContext } from "./layout.ts"; // Kepr 2's mark, in Kepr's gold. const FAVICON = readFileSync(join(import.meta.dir, "../../public/favicon.svg")); // Chromium fetches a link's page while the pointer rests on it (2026-10-02), so a click opens at // once. Never a link whose GET does something: a download, an archive or raw file, a counted feed // or update check, Koh's routes, a backup, opening a notification (marks it read), or a form whose // page sets a cookie or starts the issue form's spam timer. const SPECULATION = JSON.stringify({ prefetch: [ { where: { and: [ { href_matches: "/*" }, { not: { href_matches: [ "/*/archive/*", "/*/raw/*", "/*/releases/*", "/*/install.sh", "/*/koh/*", "/*/update-info", "/*/meta", "/*/bundle*", "/*/file", "/source", "/api/*", "/admin/backup*", "/admin/notifications/*", "/admin/sign-out", "/*/issues/*", "/sign-in*", "/connect*", ], }, }, ], }, eagerness: "moderate", }, ], }); /** §22.6: robots.txt, the sitemap (public repos only, as everywhere, §20) and the favicon. */ export function registerHousekeeping(app: Hono, ctx: PageContext): void { const origin = `https://${ctx.domain}`; app.get("/speculation.json", (c) => c.body(SPECULATION, 200, { "Content-Type": "application/speculationrules+json", "Cache-Control": "public, max-age=3600", }), ); app.get("/robots.txt", (c) => c.body(`User-agent: *\nAllow: /\n\nSitemap: ${origin}/sitemap.xml\n`, 200, { "Content-Type": "text/plain; charset=UTF-8", }), ); app.get("/sitemap.xml", (c) => { const url = (path: string, changefreq: string, priority: string) => ` ${ origin }${path}${changefreq}${priority}\n`; const urls = [ url("/", "daily", "1.0"), url("/browse", "daily", "0.9"), url("/docs", "weekly", "0.8"), // Repo names are [A-Za-z0-9._-] (§9.2's check at sync), so they need no XML escaping. ...publicRepoNames(ctx.kepr).map((name) => url(`/${name}`, "daily", "0.7")), ]; return c.body( `\n\n${urls.join( "", )}\n`, 200, { "Content-Type": "application/xml; charset=UTF-8", "Cache-Control": "public, max-age=3600" }, ); }); app.get("/favicon.svg", (c) => c.body(FAVICON, 200, { "Content-Type": "image/svg+xml", "Cache-Control": "public, max-age=86400", }), ); }