// 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 } export interface DriftInput { site: SeoSiteAnswer routes: readonly RoutePattern[] pagination: LayerPaginationOption /** seo.htmlSitemap.paths, or false when the layer serves no HTML sitemap. */ htmlSitemap: Record | 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 }