- the module writes #seo-routes (static paths and route patterns) from the resolved pages; options seo.routeReport, seo.pagination and seo.htmlSitemap reach the public runtime config - seo-route-report: POST /registry/routes once per process with the route-report token, one retry after 60 s on failure, nothing without a token, with the layer off or with routeReport false - seo-drift: one "[vue-seo-layer] drift:" warning per mismatch between the CMS's site answer (URL patterns, pagination, HTML sitemap paths) and the build - GET /api/__seo__/site and useSeoSite() - smoke grammar routereport and log, fixture registry route, drift run
85 lines
3.8 KiB
TypeScript
85 lines
3.8 KiB
TypeScript
// The startup drift check (D-20c, RESEARCH Open Question 9): Nuxt routes
|
|
// and the layer's pagination and HTML-sitemap options are fixed at build
|
|
// time, while the CMS decides content URL patterns, the pagination form and
|
|
// the HTML-sitemap paths. At server start the layer compares the two and
|
|
// logs one warning per mismatch, so an editor's change in the CMS that the
|
|
// frontend does not follow is visible. Pure, so `node --test` drives it.
|
|
|
|
import { patternCovers } from '../../lib/guard.ts'
|
|
import type { RoutePattern } from '../../lib/routes.ts'
|
|
import type { SeoSiteAnswer } from '../../app/utils/types.ts'
|
|
|
|
export const DRIFT_PREFIX = '[vue-seo-layer] drift:'
|
|
|
|
export interface LayerPaginationOption {
|
|
form: 'query' | 'path'
|
|
param: string
|
|
segments: Record<string, string>
|
|
}
|
|
|
|
export interface DriftInput {
|
|
site: SeoSiteAnswer
|
|
routes: readonly RoutePattern[]
|
|
pagination: LayerPaginationOption
|
|
/** seo.htmlSitemap.paths, or false when the layer serves no HTML sitemap. */
|
|
htmlSitemap: Record<string, string> | false
|
|
}
|
|
|
|
/** A CMS URL pattern in route-rule form: '{slug}' and ':slug' alike become ':slug'. */
|
|
const cmsPattern = (p: string) => p.replace(/\{(\w+)\}/g, ':$1').split('?')[0]!
|
|
|
|
/**
|
|
* True when a Nuxt route serves the CMS pattern: a route of the same shape
|
|
* (a parameter stands for any segment), or a catch-all under a static
|
|
* prefix ('/docs/**'). A catch-all at the root ('[...slug].vue') is a
|
|
* generic page renderer and does not count as a route for a content type.
|
|
*/
|
|
export function routeServes(route: RoutePattern, pattern: string): boolean {
|
|
if (route.catchAll && route.pattern === '/**') return false
|
|
return patternCovers(route.pattern, cmsPattern(pattern))
|
|
}
|
|
|
|
/** One message per mismatch between the CMS's site answer and the build. */
|
|
export function driftWarnings(input: DriftInput): string[] {
|
|
const out: string[] = []
|
|
const site = input.site ?? {}
|
|
|
|
for (const r of Array.isArray(site.routes) ? site.routes : []) {
|
|
if (!r || typeof r.pattern !== 'string' || r.pattern === '') continue
|
|
if (!input.routes.some((route) => routeServes(route, r.pattern))) {
|
|
out.push(`${DRIFT_PREFIX} the CMS's ${r.type || 'content'} URLs (${r.pattern}) match no Nuxt route`)
|
|
}
|
|
}
|
|
|
|
const p = site.pagination
|
|
if (p && typeof p === 'object') {
|
|
const form = p.form === 'path' ? 'path' : p.form === 'query' ? 'query' : null
|
|
if (form && form !== input.pagination.form) {
|
|
out.push(`${DRIFT_PREFIX} the CMS paginates in the ${form} form, the layer was built with ${input.pagination.form} (seo.pagination.form)`)
|
|
} else if (form === 'query' && typeof p.param === 'string' && p.param && p.param !== input.pagination.param) {
|
|
out.push(`${DRIFT_PREFIX} the CMS's page parameter is ${p.param}, the layer's is ${input.pagination.param} (seo.pagination.param)`)
|
|
} else if (form === 'path') {
|
|
for (const [locale, segment] of Object.entries(p.segments ?? {})) {
|
|
const own = input.pagination.segments[locale]
|
|
if (typeof segment === 'string' && segment && own !== segment) {
|
|
out.push(`${DRIFT_PREFIX} the CMS's ${locale} page segment is ${segment}, the layer's is ${own ?? 'unset'} (seo.pagination.segments)`)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const html = site.html_sitemap
|
|
if (html && html.enabled === true && html.paths && typeof html.paths === 'object') {
|
|
if (input.htmlSitemap === false) {
|
|
out.push(`${DRIFT_PREFIX} the CMS has an HTML sitemap, the layer serves none (seo.htmlSitemap is false)`)
|
|
} else {
|
|
for (const [locale, path] of Object.entries(html.paths)) {
|
|
if (typeof path !== 'string' || path === '') continue
|
|
const own = input.htmlSitemap[locale]
|
|
if (own !== path) out.push(`${DRIFT_PREFIX} the CMS serves the ${locale} HTML sitemap at ${path}, the layer at ${own ?? 'no path'} (seo.htmlSitemap.paths)`)
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|