- 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
63 lines
3.0 KiB
TypeScript
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)
|
|
})
|