// The routes the layer tells the CMS about (D-33) and compares with it at // server start (D-20c). Built at build time from the resolved pages, whose // meta carries requiresAuth and seo (the module adds both to Nuxt's page // meta extraction keys), and written into the server template #seo-routes // for the route report and the drift check. Pure TypeScript, so // `node --test` drives it. import { flattenPages, isNoindex, pagePattern, type GuardPage } from './guard.ts' export interface RoutePage extends GuardPage { name?: string meta?: Record children?: RoutePage[] } export interface RoutePattern { /** A route-rule style pattern: '/blog/:slug', '/docs/**', '/'. */ pattern: string /** True when the route ends in a catch-all ('[...slug]'). */ catchAll: boolean } /** True when a robots value keeps the page out of the index. */ export const robotsNoindex = (robots: unknown) => typeof robots === 'string' && /(^|[\s,])(noindex|none)($|[\s,])/i.test(robots) /** * The robots value a page asks for through its meta: seo.robots when set, * else 'noindex, nofollow' for a page with requiresAuth: true (a signed-in * or auth page, D-23), else null (the CMS decides). */ export function pageMetaRobots(meta: Record | undefined | null): string | null { if (!meta || typeof meta !== 'object') return null const seo = meta.seo if (seo && typeof seo === 'object') { const robots = (seo as { robots?: unknown }).robots if (typeof robots === 'string' && robots.trim() !== '') return robots.trim() } if (meta.requiresAuth === true) return 'noindex, nofollow' return null } /** The layer's own HTML sitemap pages, which the CMS knows already. */ const isLayerPage = (name: unknown) => typeof name === 'string' && name.startsWith('seo-sitemap-') /** * The paths without parameters that may be indexed, sorted: no page whose * meta keeps it out of the index (requiresAuth, a noindex seo.robots), none * under seo.noindexRoutes, none of the layer's HTML sitemap pages and no * client-only page. */ export function staticRoutes(pages: readonly RoutePage[], noindexRoutes: readonly string[]): string[] { const out = new Set() for (const page of flattenPages(pages) as Array) { // A parent with children renders through one of them. if (Array.isArray(page.children) && page.children.some((c) => c.path === '')) continue const path = page.fullPath if (path.includes(':') || path.includes('*')) continue if (page.mode === 'client' || isLayerPage(page.name)) continue if (robotsNoindex(pageMetaRobots(page.meta))) continue if (isNoindex(path, noindexRoutes)) continue out.add(path) } return [...out].sort() } /** Every page's route pattern ('/posts/:slug', '/**' for a catch-all), sorted. */ export function routePatterns(pages: readonly RoutePage[]): RoutePattern[] { const seen = new Map() for (const page of flattenPages(pages)) { const pattern = pagePattern(page.fullPath) if (!seen.has(pattern)) seen.set(pattern, { pattern, catchAll: pattern.endsWith('/**') }) } return [...seen.values()].sort((a, b) => a.pattern.localeCompare(b.pattern)) } /** The body of the server template #seo-routes. */ export function routesTemplate(staticPaths: readonly string[], patterns: readonly RoutePattern[]): string { return [ '// Generated by vue-seo-layer (lib/routes.ts) from the resolved pages.', `export const staticRoutes = ${JSON.stringify(staticPaths)}`, `export const routePatterns = ${JSON.stringify(patterns)}`, '', ].join('\n') }