Rebuilded sitemap generator

This commit is contained in:
2026-09-28 14:29:42 +02:00
parent b807fbe69a
commit de75d8374d
6 changed files with 70 additions and 169 deletions
+21 -20
View File
@@ -1,33 +1,32 @@
/**
* Serializes sitemap entries to an XML STRING with an XSL stylesheet reference,
* so /sitemap.xml renders as a readable table in the browser (not raw XML) while
* staying a valid sitemap for crawlers.
* Serializes sitemap entries to a clean, indented XML STRING — valid for crawlers
* and readable in the browser as the native XML tree (indented, collapsible,
* syntax-highlighted by the browser itself).
*
* Why this exists alongside the Next MetadataRoute sitemap: Next's app/sitemap.ts
* (returning SitemapEntry[]) does NOT let you inject <?xml-stylesheet?>. To get
* the styled table, serve a custom route that returns this string instead:
* NO XSLT stylesheet: browsers (Chrome et al.) are REMOVING XSLT support, so a
* <?xml-stylesheet?> approach is a dead end — it shows a deprecation warning now
* and will break soon. Instead we serve plain XML with the correct Content-Type;
* the browser renders its built-in formatted XML view (the "code tree" look) with
* no transformation needed.
*
* // app/sitemap.xml/route.ts
* import { buildSitemapXml } from '@intecion/ipal-kit'
* import { sitemap } from '@/lib/content' // your entries source
* import { sitemap } from '@/lib/content'
* export const dynamic = 'force-dynamic'
* export async function GET() {
* const entries = await sitemap()
* const xml = buildSitemapXml(entries, { stylesheetUrl: '/sitemap.xsl' })
* const xml = buildSitemapXml(entries)
* return new Response(xml, {
* headers: { 'Content-Type': 'application/xml; charset=utf-8' },
* })
* }
*
* Put sitemap.xsl in the project's /public (copy from the plugin's assets, or
* serve it from a route). The <?xml-stylesheet?> points browsers at it; crawlers
* ignore it and read the XML. Include hreflang alternates as <xhtml:link>.
* The indentation makes the raw XML pleasant to read; the browser's default XML
* viewer adds the tree/fold UI. Crawlers read the XML as usual.
*
* NOTE: if you use this custom route, DON'T also keep app/sitemap.ts — pick one
* (the styled route OR the Next MetadataRoute). Two sitemaps at different paths
* confuse crawlers.
*/ export function buildSitemapXml(entries, opts = {}) {
const { stylesheetUrl } = opts;
* (this route OR the Next MetadataRoute). Two sitemaps confuse crawlers.
*/ export function buildSitemapXml(entries) {
const esc = (s)=>s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');
const urls = entries.map((e)=>{
const parts = [
@@ -37,9 +36,12 @@
const iso = e.lastModified instanceof Date ? e.lastModified.toISOString() : String(e.lastModified);
parts.push(` <lastmod>${esc(iso)}</lastmod>`);
}
if (e.changeFrequency) parts.push(` <changefreq>${e.changeFrequency}</changefreq>`);
if (typeof e.priority === 'number') parts.push(` <priority>${e.priority}</priority>`);
// hreflang alternates
if (e.changeFrequency) {
parts.push(` <changefreq>${e.changeFrequency}</changefreq>`);
}
if (typeof e.priority === 'number') {
parts.push(` <priority>${e.priority}</priority>`);
}
const alternates = e.alternates?.languages;
if (alternates) {
for (const [lang, href] of Object.entries(alternates)){
@@ -50,8 +52,7 @@
}
return ` <url>\n${parts.join('\n')}\n </url>`;
}).join('\n');
const stylesheet = stylesheetUrl ? `<?xml-stylesheet type="text/xsl" href="${esc(stylesheetUrl)}"?>\n` : '';
return `<?xml version="1.0" encoding="UTF-8"?>\n` + stylesheet + `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" ` + `xmlns:xhtml="http://www.w3.org/1999/xhtml">\n` + urls + `\n</urlset>`;
return `<?xml version="1.0" encoding="UTF-8"?>\n` + `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" ` + `xmlns:xhtml="http://www.w3.org/1999/xhtml">\n` + urls + `\n</urlset>`;
}
//# sourceMappingURL=buildSitemapXml.js.map