// 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)`
}