Files
vue-seo-layer/server/api/__seo__/sitemap.get.ts
Jakub Zych aee6957bd6 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
2026-11-04 12:00:00 +01:00

63 lines
3.0 KiB
TypeScript

// GET /api/__seo__/sitemap?locale=&group=&page=: the HTML sitemap (D-34)
// from the sitemap plugin, for the layer's HTML sitemap pages
// (SeoHtmlSitemapPage). Without group: the overview of the locale; with
// group: one page of that group.
//
// 404 the layer is off, the backend has no site answer (an older SEO
// plugin) or the HTML sitemap is off or has no path for the
// locale, a malformed parameter, an unknown group or a page past
// the last (the plugin's 404 is passed through)
// 503 the backend did not answer or answered anything else (with
// Retry-After: 60), so the page renders an error, never an empty
// sitemap that a route cache could keep
//
// The target is the server-only backend base plus a checked group slug;
// only locale and page travel as query values. Never route-cached
// ('/api/__seo__/**' cache false) and never indexed.
import { seoServerConfig } from '../../lib/config.ts'
import { HTML_SITEMAP_GROUP, HTML_SITEMAP_LOCALE, htmlSitemapGroupPage, htmlSitemapOverview, htmlSitemapPage } from '../../lib/htmlSitemap.ts'
import { siteAnswer } from '../../lib/site.ts'
const notFound = () => createError({ statusCode: 404, statusMessage: 'Not Found' })
export default defineEventHandler(async (event) => {
setResponseHeaders(event, { 'cache-control': 'no-store', 'x-robots-tag': 'noindex' })
const unavailable = () => {
setResponseHeader(event, 'retry-after', '60')
return createError({ statusCode: 503, statusMessage: 'Sitemap unavailable' })
}
const cfg = seoServerConfig(useRuntimeConfig(event))
if (!cfg.enabled) throw notFound()
const q = getQuery(event)
const locale = typeof q.locale === 'string' ? q.locale : ''
const group = typeof q.group === 'string' ? q.group : ''
const page = htmlSitemapPage(q.page)
if (!HTML_SITEMAP_LOCALE.test(locale) || (group !== '' && !HTML_SITEMAP_GROUP.test(group)) || page === null) throw notFound()
const forwarded = event.context.seoForwardedFor as string | undefined
const headers: Record<string, string> = forwarded ? { 'x-forwarded-for': forwarded } : {}
const state = await siteAnswer(cfg, headers)
if (state.state === 'unknown') throw unavailable()
if (state.state === 'absent') throw notFound()
const html = state.site.html_sitemap
if (!html || html.enabled !== true || !html.paths || !html.paths[locale]) throw notFound()
const path = group ? `/_sitemap/api/v1/html/${encodeURIComponent(group)}` : '/_sitemap/api/v1/html'
let res: { status: number; _data?: unknown }
try {
res = await $fetch.raw(path, {
baseURL: cfg.backendBase,
query: group ? { locale, page } : { locale },
headers,
timeout: Math.max(cfg.timeoutMs, 3000),
ignoreResponseError: true,
})
} catch {
throw unavailable()
}
if (res.status === 404) throw notFound()
if (res.status !== 200 || !res._data || typeof res._data !== 'object') throw unavailable()
return group ? htmlSitemapGroupPage(res._data, group) : htmlSitemapOverview(res._data)
})