// The HTML sitemap pages the layer registers (D-34, P33-68): one overview // at seo.htmlSitemap.paths[locale], one group page at /:group and, in // the path pagination form, /:group//:page with the locale's // segment. The module (lib/seo-module.ts) calls this from a pages:resolved // hook that runs after @nuxtjs/i18n has localised the app's routes, so // i18n never copies these routes into other locales (L4 WR-05): with i18n // (any strategy but no_prefix) each route is named // , the name i18n reads the route's // locale from, and exists once, at the configured path. A path an app page // already has is the app's ('taken'); a path an app catch-all page covers // ('[...slug].vue' at the root, or under a prefix) stays the catch-all's // unless overCatchAll is true (L4 WR-04), because a static route would win // over it in vue-router and shadow the CMS page there. Pure TypeScript, so // `node --test` drives it. import { flattenPages, pagePattern, patternCovers } from './guard.ts' import type { SeoHtmlSitemapOption, SeoPaginationOption } from './options.ts' import type { RoutePage } from './routes.ts' /** Every route the layer adds starts with this name. */ export const SITEMAP_PAGE_PREFIX = 'seo-sitemap-' export interface SitemapPagesInput { htmlSitemap: SeoHtmlSitemapOption pagination: SeoPaginationOption /** The resolved pages (after @nuxtjs/i18n), the layer's own routes left out. */ pages: readonly RoutePage[] /** nuxt.options.i18n when the app configures @nuxtjs/i18n. */ i18n?: unknown /** The overview and group page components. */ files: { overview: string; group: string } } export interface SitemapRoute { name: string path: string file: string meta: { seoSitemap: { locale: string } } } export interface SitemapSkip { locale: string path: string reason: 'taken' | 'catch-all' } /** True for a route the layer added (a former run of the hook). */ export const isSitemapRoute = (page: { name?: unknown; file?: unknown }, files: { overview: string; group: string }) => typeof page.name === 'string' && page.name.startsWith(SITEMAP_PAGE_PREFIX) && (page.file === files.overview || page.file === files.group) /** The route-name suffix of a locale with @nuxtjs/i18n active, '' without. */ function localeSuffix(i18n: unknown, locale: string): string { if (!i18n || typeof i18n !== 'object') return '' const o = i18n as { strategy?: unknown; routesNameSeparator?: unknown } if (o.strategy === 'no_prefix') return '' const sep = typeof o.routesNameSeparator === 'string' && o.routesNameSeparator !== '' ? o.routesNameSeparator : '___' return `${sep}${locale}` } /** The routes to add and the locales left out, with the reason. */ export function seoSitemapPages(input: SitemapPagesInput): { routes: SitemapRoute[]; skipped: SitemapSkip[] } { const routes: SitemapRoute[] = [] const skipped: SitemapSkip[] = [] const html = input.htmlSitemap if (html === false) return { routes, skipped } const pages = flattenPages(input.pages).filter((p) => !isSitemapRoute(p as RoutePage, input.files)) const taken = new Set(pages.map((p) => p.fullPath)) const catchAlls = pages.map((p) => pagePattern(p.fullPath)).filter((p) => p.endsWith('/**')) for (const [locale, path] of Object.entries(html.paths)) { if (taken.has(path)) { skipped.push({ locale, path, reason: 'taken' }) continue } if (!html.overCatchAll && catchAlls.some((pattern) => patternCovers(pattern, path))) { skipped.push({ locale, path, reason: 'catch-all' }) continue } const suffix = localeSuffix(input.i18n, locale) const meta = { seoSitemap: { locale } } routes.push({ name: `${SITEMAP_PAGE_PREFIX}${locale}${suffix}`, path, file: input.files.overview, meta }) routes.push({ name: `${SITEMAP_PAGE_PREFIX}group-${locale}${suffix}`, path: `${path}/:group`, file: input.files.group, meta }) if (input.pagination.form === 'path') { const segment = input.pagination.segments[locale] || input.pagination.param routes.push({ name: `${SITEMAP_PAGE_PREFIX}group-page-${locale}${suffix}`, path: `${path}/:group/${segment}/:page(\\d+)`, file: input.files.group, meta }) } } return { routes, skipped } } /** The build log line of a skipped locale. */ export function sitemapSkipMessage(skip: SitemapSkip): string { if (skip.reason === 'taken') return `HTML sitemap: the app has its own page at ${skip.path}; the layer's ${skip.locale} sitemap page is not registered` return `HTML sitemap: ${skip.locale} ${skip.path} is rendered by the app's catch-all page; the layer's page is not registered (set seo.htmlSitemap.overCatchAll to serve it ahead of the catch-all)` }