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",
}),
);
}