36 lines
1.6 KiB
TypeScript
36 lines
1.6 KiB
TypeScript
// 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.
|
|
|
|
import { seoVerificationName } from '../../app/utils/seo.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 config = useRuntimeConfig(event)
|
|
if ((config.public.seo as { enabled?: boolean } | undefined)?.enabled === false) return
|
|
const seo = config.seo as { apiBase: string; timeoutMs?: number }
|
|
const res = await $fetch
|
|
.raw(`/verification/${encodeURIComponent(name)}`, {
|
|
baseURL: seo.apiBase,
|
|
responseType: 'text',
|
|
ignoreResponseError: true,
|
|
timeout: Number(seo.timeoutMs) || 1500,
|
|
})
|
|
.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)
|
|
})
|