housekeeping.ts

2.7 KB · raw

1import { readFileSync } from "node:fs";2import { join } from "node:path";3import type { Hono } from "hono";4import { publicRepoNames } from "../server/db.ts";5import type { PageContext } from "./layout.ts";67// Kepr 2's mark, in Kepr's gold.8const FAVICON = readFileSync(join(import.meta.dir, "../../public/favicon.svg"));910// Chromium fetches a link's page while the pointer rests on it (2026-10-02), so a click opens at11// once. Never a link whose GET does something: a download, an archive or raw file, a counted feed12// or update check, Koh's routes, a backup, opening a notification (marks it read), or a form whose13// page sets a cookie or starts the issue form's spam timer.14const SPECULATION = JSON.stringify({15	prefetch: [16		{17			where: {18				and: [19					{ href_matches: "/*" },20					{21						not: {22							href_matches: [23								"/*/archive/*",24								"/*/raw/*",25								"/*/releases/*",26								"/*/install.sh",27								"/*/koh/*",28								"/*/update-info",29								"/*/meta",30								"/*/bundle*",31								"/*/file",32								"/source",33								"/api/*",34								"/admin/backup*",35								"/admin/notifications/*",36								"/admin/sign-out",37								"/*/issues/*",38								"/sign-in*",39								"/connect*",40							],41						},42					},43				],44			},45			eagerness: "moderate",46		},47	],48});4950/** §22.6: robots.txt, the sitemap (public repos only, as everywhere, §20) and the favicon. */51export function registerHousekeeping(app: Hono, ctx: PageContext): void {52	const origin = `https://${ctx.domain}`;53	app.get("/speculation.json", (c) =>54		c.body(SPECULATION, 200, {55			"Content-Type": "application/speculationrules+json",56			"Cache-Control": "public, max-age=3600",57		}),58	);59	app.get("/robots.txt", (c) =>60		c.body(`User-agent: *\nAllow: /\n\nSitemap: ${origin}/sitemap.xml\n`, 200, {61			"Content-Type": "text/plain; charset=UTF-8",62		}),63	);64	app.get("/sitemap.xml", (c) => {65		const url = (path: string, changefreq: string, priority: string) =>66			`  <url><loc>${67				origin68			}${path}</loc><changefreq>${changefreq}</changefreq><priority>${priority}</priority></url>\n`;69		const urls = [70			url("/", "daily", "1.0"),71			url("/browse", "daily", "0.9"),72			url("/docs", "weekly", "0.8"),73			// Repo names are [A-Za-z0-9._-] (§9.2's check at sync), so they need no XML escaping.74			...publicRepoNames(ctx.kepr).map((name) => url(`/${name}`, "daily", "0.7")),75		];76		return c.body(77			`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls.join(78				"",79			)}</urlset>\n`,80			200,81			{ "Content-Type": "application/xml; charset=UTF-8", "Cache-Control": "public, max-age=3600" },82		);83	});84	app.get("/favicon.svg", (c) =>85		c.body(FAVICON, 200, {86			"Content-Type": "image/svg+xml",87			"Cache-Control": "public, max-age=86400",88		}),89	);90}