- 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
56 lines
2.2 KiB
TypeScript
56 lines
2.2 KiB
TypeScript
// Reports every page answered 404 to the SEO plugin's 404 log
|
|
// (POST /notfound with the bearer token). A page that throws a 404 (an
|
|
// unknown route, a missing record) goes through Nitro's error path, which
|
|
// skips afterResponse, so both the `error` hook and `afterResponse` report,
|
|
// once per request. Only page views count: GET requests that accept
|
|
// text/html, never Nuxt's own paths (/_nuxt, /__nuxt_error, …) or the API
|
|
// routes, and nothing without the token configured or with the layer off
|
|
// (public.seo.enabled or seo.cms false). The report never delays the
|
|
// response: it is sent after it (event.waitUntil keeps it alive). A failed
|
|
// report is logged once and never touches the response.
|
|
|
|
import type { H3Event } from 'h3'
|
|
import { seoResolvableUrl } from '../../app/utils/seo.ts'
|
|
import { seoServerConfig } from '../lib/config.ts'
|
|
|
|
let warned = false
|
|
|
|
function report(event: H3Event) {
|
|
if (event.context.seoNotFoundReported) return
|
|
if (event.method !== 'GET') return
|
|
if (!(getRequestHeader(event, 'accept') ?? '').includes('text/html')) return
|
|
const seo = seoServerConfig(useRuntimeConfig(event))
|
|
if (!seo.enabled) return
|
|
const token = seo.notfoundToken
|
|
if (!token) return
|
|
const url = seoResolvableUrl(event.path)
|
|
if (!url) return
|
|
event.context.seoNotFoundReported = true
|
|
|
|
const referer = getRequestHeader(event, 'referer') ?? ''
|
|
const forwarded = event.context.seoForwardedFor as string | undefined
|
|
const sent = $fetch('/notfound', {
|
|
baseURL: seo.apiBase,
|
|
method: 'POST',
|
|
body: { url: url.slice(0, 2048), referer: referer.slice(0, 1024) },
|
|
headers: { authorization: `Bearer ${token}`, ...(forwarded ? { 'x-forwarded-for': forwarded } : {}) },
|
|
timeout: seo.timeoutMs,
|
|
}).catch((err: unknown) => {
|
|
if (!warned) {
|
|
warned = true
|
|
console.warn('[vue-seo-layer] 404 report failed:', (err as Error)?.message ?? err)
|
|
}
|
|
})
|
|
event.waitUntil?.(sent)
|
|
}
|
|
|
|
export default defineNitroPlugin((nitroApp) => {
|
|
nitroApp.hooks.hook('error', (error, { event }) => {
|
|
const status = (error as { statusCode?: number } | null)?.statusCode
|
|
if (event && status === 404) report(event)
|
|
})
|
|
nitroApp.hooks.hook('afterResponse', (event) => {
|
|
if (event.node.res.statusCode === 404) report(event)
|
|
})
|
|
})
|