Files
vue-seo-layer/server/middleware/seo.ts
Jakub Zych e3baa7028c feat: redirect and 410 enforcement, X-Robots-Tag and the resolve cache
- the Nitro middleware answers redirect (301/302, Location from the answer
  only, same-site targets as paths as in v0.1.1) and gone (410) before the
  render and the route cache, sets X-Robots-Tag, and answers 404 for a
  pagination.invalid answer; other not_found answers stay with Nuxt
- resolve answers cached in an LRU (seo.resolveCacheTtl, default 60 s,
  1000 entries) with in-flight de-duplication; a clear detaches loads in
  flight so no answer fetched before a purge is stored after it
- the playground route-caches /blog/** (swr 300)
2026-11-04 09:20:20 +01:00

48 lines
2.2 KiB
TypeScript

// Resolves every page request through the CMS (resolveCached: LRU and the
// visitor's X-Forwarded-For) and enforces the decision before Nuxt renders
// the page and before Nitro's route cache, so swr-cached pages honour a new
// redirect or 410:
//
// redirect 301 or 302 with Location from the answer (seoRedirectTarget:
// a target on the site's own origin becomes a path, another
// host stays absolute); nothing from the request (Host, path,
// query) ever goes into the Location
// gone 410 with the app's error page
// not_found left to Nuxt's routing, except a pagination.invalid answer
// (?page=abc), which is a 404
//
// X-Robots-Tag follows the answer (staging: noindex, nofollow everywhere;
// else the page's x_robots_tag), never on a redirect. Skipped: Nuxt's and
// the layers' internal paths (/_nuxt, /__nuxt_error, /_seo …), /api and
// asset files (isPagePath). The answer stays in event.context.seo for the
// render, which then makes no second call (useSeoResolve). With the CMS
// unreachable the page renders normally (fail open).
import { isPagePath } from '../../app/utils/pagePath.ts'
import { seoRedirectTarget, seoXRobotsTag } from '../../app/utils/seo.ts'
import { seoServerConfig } from '../lib/config.ts'
import { resolveCached } from '../lib/resolve.ts'
export default defineEventHandler(async (event) => {
if (event.method !== 'GET' && event.method !== 'HEAD') return
const cfg = seoServerConfig(useRuntimeConfig(event))
if (!cfg.enabled) return
const url = event.path
if (!isPagePath(url, cfg.resolveFiles)) return
const answer = await resolveCached(event, url)
event.context.seo = answer
event.context.seoResolved = { url, answer }
if (!answer) return
const target = seoRedirectTarget(answer)
if (target) return sendRedirect(event, target.to, target.code)
const robots = seoXRobotsTag(answer)
if (robots) setResponseHeader(event, 'x-robots-tag', robots)
if (answer.action === 'gone') throw createError({ statusCode: 410, statusMessage: 'Gone' })
if (answer.action === 'not_found' && answer.pagination?.invalid === true) {
throw createError({ statusCode: 404, statusMessage: 'Page Not Found' })
}
})