Files
vue-seo-layer/tests/sitemapPages.test.ts
Jakub Zych a651de4675 fix: HTML sitemap pages once per locale, never over a catch-all by default
- 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
2026-11-04 22:47:58 +01:00

111 lines
6.4 KiB
TypeScript

import assert from 'node:assert/strict'
import test from 'node:test'
import { htmlSitemapOption } from '../lib/options.ts'
import { SITEMAP_PAGE_PREFIX, seoSitemapPages, type SitemapPagesInput } from '../lib/sitemapPages.ts'
const files = { overview: '/layer/SitemapPage.vue', group: '/layer/SitemapGroupPage.vue' }
const query = { form: 'query' as const, param: 'page', segments: { pl: 'strona', en: 'page' } }
const path = { form: 'path' as const, param: 'page', segments: { pl: 'strona', en: 'page' } }
const input = (over: Partial<SitemapPagesInput> = {}): SitemapPagesInput => ({
htmlSitemap: htmlSitemapOption({ paths: { pl: '/mapa-strony', en: '/sitemap' } }),
pagination: query,
pages: [{ path: '/', name: 'index' }],
files,
...over,
})
const summary = (routes: Array<{ name?: string; path: string }>) => routes.map((r) => `${r.name} ${r.path}`)
test('seoSitemapPages: without i18n, one overview and one group page per locale', () => {
const out = seoSitemapPages(input())
assert.deepEqual(summary(out.routes), [
'seo-sitemap-pl /mapa-strony',
'seo-sitemap-group-pl /mapa-strony/:group',
'seo-sitemap-en /sitemap',
'seo-sitemap-group-en /sitemap/:group',
])
assert.deepEqual(out.skipped, [])
assert.deepEqual(out.routes[0]!.meta, { seoSitemap: { locale: 'pl' } })
assert.equal(out.routes[0]!.file, files.overview)
assert.equal(out.routes[1]!.file, files.group)
assert.ok(out.routes.every((r) => r.name!.startsWith(SITEMAP_PAGE_PREFIX)))
})
test('seoSitemapPages: the path form adds the page route with the locale segment', () => {
const out = seoSitemapPages(input({ pagination: path }))
assert.deepEqual(summary(out.routes).filter((s) => s.includes('group-page')), [
'seo-sitemap-group-page-pl /mapa-strony/:group/strona/:page(\\d+)',
'seo-sitemap-group-page-en /sitemap/:group/page/:page(\\d+)',
])
// a locale without a segment falls back to the param
const de = seoSitemapPages(input({ pagination: path, htmlSitemap: htmlSitemapOption({ paths: { de: '/seitenuebersicht' } }) }))
assert.ok(summary(de.routes).includes('seo-sitemap-group-page-de /seitenuebersicht/:group/page/:page(\\d+)'))
})
test('seoSitemapPages: with i18n each route is named for its own locale, once, at the configured path (L4 WR-05)', () => {
const i18n = { strategy: 'prefix_except_default', defaultLocale: 'en', locales: [{ code: 'en' }, { code: 'pl' }] }
const out = seoSitemapPages(input({ i18n, htmlSitemap: htmlSitemapOption({ paths: { en: '/sitemap', pl: '/pl/mapa-strony' } }) }))
assert.deepEqual(summary(out.routes), [
'seo-sitemap-en___en /sitemap',
'seo-sitemap-group-en___en /sitemap/:group',
'seo-sitemap-pl___pl /pl/mapa-strony',
'seo-sitemap-group-pl___pl /pl/mapa-strony/:group',
])
// no copy under another locale's name or prefix
assert.ok(!out.routes.some((r) => /___pl$/.test(r.name!) && r.meta.seoSitemap.locale !== 'pl'))
assert.ok(!out.routes.some((r) => r.path.startsWith('/pl/sitemap')))
// a custom separator; no_prefix leaves the names alone
const sep = seoSitemapPages(input({ i18n: { ...i18n, routesNameSeparator: '__' } }))
assert.ok(sep.routes.every((r) => r.name!.endsWith(`__${r.meta.seoSitemap.locale}`)))
const noPrefix = seoSitemapPages(input({ i18n: { ...i18n, strategy: 'no_prefix' } }))
assert.deepEqual(noPrefix.routes.map((r) => r.name), ['seo-sitemap-pl', 'seo-sitemap-group-pl', 'seo-sitemap-en', 'seo-sitemap-group-en'])
})
test('seoSitemapPages: an app page at the path keeps it (taken)', () => {
const out = seoSitemapPages(input({ pages: [{ path: '/', name: 'index' }, { path: '/mapa-strony', name: 'mapa-strony' }] }))
assert.deepEqual(out.skipped, [{ locale: 'pl', path: '/mapa-strony', reason: 'taken' }])
assert.deepEqual(out.routes.map((r) => r.name), ['seo-sitemap-en', 'seo-sitemap-group-en'])
// a nested page with the same full path is taken too
const nested = seoSitemapPages(input({ pages: [{ path: '/sitemap', name: 'sm', children: [{ path: '', name: 'sm-index' }] }] }))
assert.deepEqual(nested.skipped.map((s) => s.locale), ['en'])
})
test('seoSitemapPages: a root catch-all skips every locale unless overCatchAll (L4 WR-04)', () => {
const pages = [{ path: '/', name: 'index' }, { path: '/:slug(.*)*', name: 'slug' }]
const out = seoSitemapPages(input({ pages }))
assert.deepEqual(out.routes, [])
assert.deepEqual(out.skipped, [
{ locale: 'pl', path: '/mapa-strony', reason: 'catch-all' },
{ locale: 'en', path: '/sitemap', reason: 'catch-all' },
])
const over = seoSitemapPages(input({ pages, htmlSitemap: htmlSitemapOption({ paths: { pl: '/mapa-strony', en: '/sitemap' }, overCatchAll: true }) }))
assert.equal(over.routes.length, 4)
assert.deepEqual(over.skipped, [])
})
test('seoSitemapPages: a prefixed catch-all skips only the paths it covers', () => {
const pages = [{ path: '/pl/:slug(.*)*', name: 'pl-slug' }]
const out = seoSitemapPages(input({ pages, htmlSitemap: htmlSitemapOption({ paths: { pl: '/pl/mapa-strony', en: '/sitemap' } }) }))
assert.deepEqual(out.skipped, [{ locale: 'pl', path: '/pl/mapa-strony', reason: 'catch-all' }])
assert.deepEqual(out.routes.map((r) => r.name), ['seo-sitemap-en', 'seo-sitemap-group-en'])
// the i18n copy of a root catch-all (/pl/... and /...) covers both
const localized = [{ path: '/:slug(.*)*', name: 'slug___en' }, { path: '/pl/:slug(.*)*', name: 'slug___pl' }]
assert.equal(seoSitemapPages(input({ pages: localized })).routes.length, 0)
// a page with one parameter is not a catch-all
assert.equal(seoSitemapPages(input({ pages: [{ path: '/:slug', name: 'slug' }] })).routes.length, 4)
})
test('seoSitemapPages: htmlSitemap false answers nothing; earlier layer routes are not "taken"', () => {
assert.deepEqual(seoSitemapPages(input({ htmlSitemap: false })), { routes: [], skipped: [] })
const again = seoSitemapPages(input({ pages: [{ path: '/mapa-strony', name: 'seo-sitemap-pl', file: files.overview }] }))
assert.equal(again.routes.length, 4)
assert.deepEqual(again.skipped, [])
})
test('htmlSitemapOption: overCatchAll is a boolean, default false', () => {
assert.deepEqual(htmlSitemapOption({ paths: { pl: '/mapa-strony' } }), { paths: { pl: '/mapa-strony' }, overCatchAll: false })
assert.deepEqual(htmlSitemapOption({ paths: { pl: '/mapa-strony' }, overCatchAll: true }), { paths: { pl: '/mapa-strony' }, overCatchAll: true })
assert.deepEqual(htmlSitemapOption({ paths: {}, overCatchAll: 'yes' }), { paths: {}, overCatchAll: false })
})