feat: HTML sitemap pages and footer link

- the module registers seo-sitemap-<locale> at seo.htmlSitemap.paths
  (defaults pl /mapa-strony, en /sitemap) and seo-sitemap-group-<locale>
  at <path>/:group (plus <path>/:group/<segment>/:page in the path form);
  a path an app page already has stays the app's
- SeoHtmlSitemapPage (global, overridable): H1, an H2 per non-empty
  group, categories nested, a large group's link to its group page,
  sitemap.empty; group pages with a built trail, page suffix and
  <SeoPagination>; 404 for an unknown group or a page past the last,
  301 for ?page=1, 503 when the sitemap API fails
- GET /api/__seo__/sitemap proxies the sitemap plugin's HTML API (only
  while the CMS has the HTML sitemap on for the locale)
- <SeoSitemapLink>: the footer link while the HTML sitemap is on
- L3 default styles
This commit is contained in:
Jakub Zych
2026-11-04 12:00:00 +01:00
parent f2d13d6264
commit aee6957bd6
18 changed files with 600 additions and 3 deletions

View File

@@ -19,6 +19,12 @@
// GET /_sitemap/files/<file> <dir>/seo/root/<file>, else 404;
// sitemap-fail-*.xml answers 500
// GET /_sitemap/indexnow/<key>.txt <dir>/seo/indexnow/<key>.txt, else 404
// GET /_sitemap/api/v1/html?locale= the HTML sitemap overview from
// <dir>/seo/html.json (overview.<locale>),
// else 404
// GET /_sitemap/api/v1/html/<group> one group page (groups.<locale>.<group>.<page>,
// ?locale=&page= page 1 by default), else 404; the
// group fail answers 500
//
// It records every request (with the parsed JSON body of a POST) so a smoke
// run can assert header forwarding, resolve counts and the 404 reports.
@@ -65,7 +71,10 @@ export function startFixtureApi({ dir, port = 0, token = FIXTURE_NOTFOUND_TOKEN,
let answers = {}
/** @type {unknown} */
let siteAnswer = null
/** @type {any} */
let html = {}
const load = () => {
html = readJson(join(dir, 'seo', 'html.json')) ?? {}
answers = { ...(readJson(join(dir, 'resolve.json')) ?? {}), ...(readJson(join(dir, 'seo', 'resolve.json')) ?? {}) }
siteAnswer = site ? readJson(join(dir, 'seo', siteFile)) : null
}
@@ -90,6 +99,13 @@ export function startFixtureApi({ dir, port = 0, token = FIXTURE_NOTFOUND_TOKEN,
if (files && req.method === 'GET') return fileAnswer(join(dir, 'seo', 'root', files[1]), files[1])
const key = /^\/_sitemap\/indexnow\/([A-Za-z0-9-]{8,128}\.txt)$/.exec(url.pathname)
if (key && req.method === 'GET') return fileAnswer(join(dir, 'seo', 'indexnow', key[1]), key[1])
const htmlGroup = /^\/_sitemap\/api\/v1\/html(?:\/([a-z0-9-]+))?$/.exec(url.pathname)
if (htmlGroup && req.method === 'GET') {
const locale = url.searchParams.get('locale') ?? ''
if (htmlGroup[1] === 'fail') return { status: 500, body: { error: 'backend failure' } }
const hit = htmlGroup[1] ? html.groups?.[locale]?.[htmlGroup[1]]?.[url.searchParams.get('page') ?? '1'] : html.overview?.[locale]
return hit ? { status: 200, body: hit } : { status: 404, body: { error: 'not_found' } }
}
if (!url.pathname.startsWith(PREFIX + '/')) return { status: 404, body: { error: 'not_found' } }
const path = url.pathname.slice(PREFIX.length)
if (path === '/resolve' && req.method === 'GET') {