// The HTML sitemap answers of the sitemap plugin (GET // /_sitemap/api/v1/html?locale= and // /_sitemap/api/v1/html/{group}?locale=&page=) reduced to the fields the // layer renders, plus the checks of the layer's proxy route. Pure, so // `node --test` drives it. import type { SeoSitemapEntry, SeoSitemapGroup, SeoSitemapGroupPage, SeoSitemapOverview } from '../../app/utils/types.ts' export const HTML_SITEMAP_LOCALE = /^[a-z]{2,3}(?:[-_][A-Za-z0-9]{2,8})*$/ export const HTML_SITEMAP_GROUP = /^[a-z0-9][a-z0-9-]{0,63}$/ /** Nesting deeper than this (a category tree) is cut off. */ const MAX_DEPTH = 8 const str = (v: unknown) => (typeof v === 'string' ? v : '') const int = (v: unknown) => (typeof v === 'number' && Number.isInteger(v) && v >= 0 ? v : 0) function entries(value: unknown, depth = 0): SeoSitemapEntry[] { if (!Array.isArray(value) || depth > MAX_DEPTH) return [] const out: SeoSitemapEntry[] = [] for (const e of value) { if (!e || typeof e !== 'object') continue const o = e as Record const title = str(o.title) if (!title) continue const children = entries(o.children, depth + 1) out.push(children.length > 0 ? { title, url: str(o.url), children } : { title, url: str(o.url) }) } return out } /** The overview with only its non-empty groups. */ export function htmlSitemapOverview(value: unknown): SeoSitemapOverview { const groups: SeoSitemapGroup[] = [] const raw = value && typeof value === 'object' ? (value as { groups?: unknown }).groups : null for (const g of Array.isArray(raw) ? raw : []) { if (!g || typeof g !== 'object') continue const o = g as Record const slug = str(o.slug) const list = entries(o.entries) if (!HTML_SITEMAP_GROUP.test(slug) || list.length === 0) continue groups.push({ key: str(o.key) || slug, slug, title: str(o.title) || slug, total: int(o.total) || list.length, entries: list, more: o.more === true }) } return { groups } } /** One page of a group. */ export function htmlSitemapGroupPage(value: unknown, group: string): SeoSitemapGroupPage { const o = value && typeof value === 'object' ? (value as Record) : {} const page = int(o.page) || 1 return { group, title: str(o.title) || group, page, pages: Math.max(int(o.pages), page), entries: entries(o.entries) } } /** A page query value: 1 when absent, the number for 1 to 999999, else null. */ export function htmlSitemapPage(raw: unknown): number | null { if (raw === undefined || raw === null || raw === '') return 1 if (typeof raw !== 'string' || !/^\d{1,6}$/.test(raw)) return null const n = Number(raw) return n >= 1 ? n : null }