/** The robots.txt body, read from the site's own state. * * The public site feature decides which pages a crawler can read. While it * is off, only the listings pages are open, and the file names no sitemap. * Once it is on, every page the site serves is open, and the file points at * the site's sitemap. The private areas lean on their noindex headers and * their auth. The file names no path for a crawler to probe. */ import { isMissingSettingsTableError } from "#db/migrations/errors.ts"; import { settings } from "#db/settings.ts"; import { loadPublicDomain, publicCacheResponse, } from "#routes/public-origin.ts"; import { TEXT } from "#shared/content-types.ts"; import { CONFIG_KEYS } from "#shared/settings/keys.ts"; const PRIVATE_ROBOTS_TXT = "User-agent: *\nAllow: /listings/\nDisallow: /\n"; const PUBLIC_ROBOTS_TXT = "User-agent: *\nAllow: /\n"; /** Serve robots.txt from the site's own state. The static path serves before * the request's settings load, so the body decision loads its own keys: both * fresh and declared. The cache is short, because the body follows a * setting an operator can flip. */ export const handleRobotsTxt = async (request: Request): Promise => { // The default body stands until the settings load succeeds. A failed load // can leave a stale snapshot behind, and a table-less site is treated as // off no matter what that snapshot last said. let body = PRIVATE_ROBOTS_TXT; try { await settings.loadKeys([CONFIG_KEYS.ENABLED_FEATURES]); if (settings.features.site) { const domain = await loadPublicDomain(new URL(request.url).hostname); body = `${PUBLIC_ROBOTS_TXT}Sitemap: https://${domain}/sitemap.xml\n`; } } catch (error) { // A site before setup has no settings table, so the feature cannot be // on and the default body is the answer. Every other failure stays loud. if (!isMissingSettingsTableError(error)) throw error; } return publicCacheResponse(body, TEXT); };