- seoSitemapPages (lib/sitemapPages.ts) builds the routes per locale; the module adds them from a pages:resolved hook registered on ready, after @nuxtjs/i18n has localised the app's routes, so no cross-locale copies exist and each name ends in the locale's i18n suffix (L4 WR-05) - a path an app catch-all covers is left to it and logged at build unless seo.htmlSitemap.overCatchAll is true (L4 WR-04) - SitemapLink falls back to the i18n locale and renders only when the path resolves to the layer's sitemap page of that locale - an app with only app.vue no longer gets its router turned on by the pages
95 lines
4.6 KiB
TypeScript
95 lines
4.6 KiB
TypeScript
// The HTML sitemap pages the layer registers (D-34, P33-68): one overview
|
|
// at seo.htmlSitemap.paths[locale], one group page at <path>/:group and, in
|
|
// the path pagination form, <path>/:group/<segment>/: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
|
|
// <name><routesNameSeparator><locale>, 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)`
|
|
}
|