- noindexStaticRoutes: requiresAuth, noindex/none seo.robots and static
pages under seo.noindexRoutes, i18n copies included, never patterns or
the layer's HTML sitemap pages (G1, gap-2 contract)
- #seo-routes exports noindexPaths; routeReportBody(paths, noindex) answers
{routes, noindex} with the same limits and keeps the JSON body within the
older plugin's 4,113,024 bytes, dropping noindex entries first
- smoke grammar noindex= and lacksnoindex=; README carries the contract
128 lines
6.3 KiB
TypeScript
128 lines
6.3 KiB
TypeScript
// 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<string, unknown>
|
|
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<string, unknown> | 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 route-name suffixes of @nuxtjs/i18n's copies of a page in the locales
|
|
* other than the default one (`contact___pl` with the default separator):
|
|
* with prefix_except_default or prefix_and_default the CMS knows those
|
|
* addresses as the page's translations, not as pages of their own. Nothing
|
|
* without i18n, with no_prefix or without a default locale.
|
|
*/
|
|
export function localeCopySuffixes(i18n: unknown): string[] {
|
|
if (!i18n || typeof i18n !== 'object') return []
|
|
const o = i18n as { strategy?: unknown; defaultLocale?: unknown; locales?: unknown; routesNameSeparator?: unknown }
|
|
const strategy = typeof o.strategy === 'string' ? o.strategy : 'prefix_except_default'
|
|
const def = typeof o.defaultLocale === 'string' ? o.defaultLocale : ''
|
|
if (strategy === 'no_prefix' || def === '' || !Array.isArray(o.locales)) return []
|
|
const sep = typeof o.routesNameSeparator === 'string' && o.routesNameSeparator !== '' ? o.routesNameSeparator : '___'
|
|
const codes = o.locales.map((l) => (typeof l === 'string' ? l : l && typeof l === 'object' ? (l as { code?: unknown }).code : undefined))
|
|
return codes.filter((c): c is string => typeof c === 'string' && c !== '' && c !== def).map((c) => `${sep}${c}`)
|
|
}
|
|
|
|
/**
|
|
* 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, no
|
|
* client-only page and no i18n copy of a page in another locale (a route
|
|
* whose name ends in one of localeCopies, see localeCopySuffixes).
|
|
*/
|
|
export function staticRoutes(pages: readonly RoutePage[], noindexRoutes: readonly string[], localeCopies: readonly string[] = []): string[] {
|
|
const out = new Set<string>()
|
|
for (const page of flattenPages(pages) as Array<RoutePage & { fullPath: string }>) {
|
|
if (typeof page.name === 'string' && localeCopies.some((suffix) => page.name!.endsWith(suffix))) continue
|
|
// 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()
|
|
}
|
|
|
|
/**
|
|
* The paths without parameters the build serves noindex, sorted (G1, the
|
|
* route report's `noindex` list): pages whose meta keeps them out of the
|
|
* index (requiresAuth, a noindex or none seo.robots) and static pages under
|
|
* seo.noindexRoutes. Client-only pages count (they are noindex by the
|
|
* guard's rule) and so do the @nuxtjs/i18n copies in other locales, which
|
|
* are listed as their own locale-prefixed paths ('/pl/konto'). Never a
|
|
* pattern, never a path with a parameter or a catch-all, never one of the
|
|
* layer's HTML sitemap pages, and never a path staticRoutes lists.
|
|
*/
|
|
export function noindexStaticRoutes(pages: readonly RoutePage[], noindexRoutes: readonly string[]): string[] {
|
|
const out = new Set<string>()
|
|
for (const page of flattenPages(pages) as Array<RoutePage & { fullPath: string }>) {
|
|
if (Array.isArray(page.children) && page.children.some((c) => c.path === '')) continue
|
|
const path = page.fullPath
|
|
if (path.includes(':') || path.includes('*')) continue
|
|
if (isLayerPage(page.name)) continue
|
|
if (robotsNoindex(pageMetaRobots(page.meta)) || isNoindex(path, noindexRoutes)) 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<string, RoutePattern>()
|
|
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[], noindexPaths: readonly string[] = []): 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)}`,
|
|
`export const noindexPaths = ${JSON.stringify(noindexPaths)}`,
|
|
'',
|
|
].join('\n')
|
|
}
|