Files
vue-seo-layer/server/middleware/seo-verification.ts
Jakub Zych d1629439c7 feat: root files from the CMS and the 404 report
- server/middleware/seo-root.ts proxies /robots.txt, /sitemap.xml,
  /sitemap-<name>.xml, /llms.txt and the verification and IndexNow key
  files the site answer lists in root_files, with the backend's status,
  content type and X-Robots-Tag; a backend failure is 503 Retry-After 60
- server/lib/site.ts caches GET /_seo/api/v1/site (300 s, cleared by the
  purge); a backend without it (404) keeps the v0.1.1 behaviour: no root
  proxies, the verification lookup by name
- the 404 report and the verification lookup honour seo.cms and rootFiles
- smoke runs for the layer off (seo.cms and the v0.1.1 enabled switch), the
  CMS down (fail open) and no 404 token; npm run purge:check
2026-11-04 09:26:00 +01:00

46 lines
2.0 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.
//
// 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)
})