// Serves the verification files stored in the SEO settings at the site root // (/google0123.html, /BingSiteAuth.xml …). The plugin answers them under // GET /verification/{name} and registers no root route, so the frontend maps // them. Only a single-segment .html, .txt or .xml path is looked up, never a // name the site serves itself; anything the backend does not answer 200 // falls through to the normal routes. // // This is the v0.1.1 lookup, for a backend without the site answer (SEO // plugin before v0.2.0, /site answers 404) or one that does not answer it // now. With a site answer, seo-root.ts serves exactly the files its // root_files lists and every other name passes through untouched. import { seoVerificationName } from '../../app/utils/seo.ts' import { seoServerConfig } from '../lib/config.ts' import { seoForwardedFor } from '../lib/resolve.ts' import { siteAnswer } from '../lib/site.ts' const RESERVED = ['robots.txt', 'sitemap.xml', 'sitemap.xsl', 'rss.xml', 'feed.xml', 'atom.xml', 'llms.txt'] export default defineEventHandler(async (event) => { if (event.method !== 'GET' && event.method !== 'HEAD') return const name = seoVerificationName(event.path, RESERVED) if (!name) return const cfg = seoServerConfig(useRuntimeConfig(event)) if (!cfg.enabled || !cfg.rootFiles) return const headers = seoForwardedFor(event) if ((await siteAnswer(cfg, headers)).state === 'ok') return const res = await $fetch .raw(`/verification/${encodeURIComponent(name)}`, { baseURL: cfg.apiBase, responseType: 'text', ignoreResponseError: true, headers, timeout: cfg.timeoutMs, }) .catch(() => null) if (!res || res.status !== 200) return setResponseHeaders(event, { 'content-type': res.headers.get('content-type') ?? 'text/plain; charset=utf-8', 'x-content-type-options': 'nosniff', 'content-security-policy': res.headers.get('content-security-policy') ?? "default-src 'none'; sandbox", 'x-robots-tag': 'noindex', }) return event.method === 'HEAD' ? '' : (res._data as string) })