feat: vue-seo-layer, the Nuxt half of the SummerCMS SEO plugin

This commit is contained in:
Jakub Zych
2026-10-31 13:45:37 +01:00
commit ac59f3c80d
30 changed files with 7962 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
// Resolves every navigation before its page runs, so a redirect or a 410
// wins over the page (also for an old address no page matches). The
// answer is kept in shared state for the head plugin; on hydration the
// server's answer comes with the payload and nothing is fetched again.
import { seoRedirectTarget, seoResolvableUrl, seoXRobotsTag } from '../utils/seo.ts'
export default defineNuxtRouteMiddleware(async (to) => {
const nuxtApp = useNuxtApp()
const config = useRuntimeConfig()
if ((config.public.seo as { enabled?: boolean } | undefined)?.enabled === false) return
if (import.meta.client && nuxtApp.isHydrating && nuxtApp.payload.serverRendered) return
const state = useSeoAnswer()
const url = seoResolvableUrl(to.fullPath)
if (!url) {
state.value = null
return
}
const answer = await resolveSeo(url)
state.value = answer
if (!answer) return
if (import.meta.server) {
const header = seoXRobotsTag(answer)
if (header) useResponseHeader('X-Robots-Tag').value = header
}
// On the server a redirect only sends Location (as external), so the
// target page is not rendered and its headers stay off the redirect.
const target = seoRedirectTarget(answer)
if (target) {
if (import.meta.server) useResponseHeader('X-Robots-Tag').value = undefined
return navigateTo(target.to, { redirectCode: target.code, external: target.external || import.meta.server })
}
if (answer.action === 'gone') {
return abortNavigation(createError({ statusCode: 410, statusMessage: 'Gone', fatal: true }))
}
})