From de75d8374d30410cae575d62c019c1ff0d8bbedb Mon Sep 17 00:00:00 2001 From: rasm-its Date: Mon, 28 Sep 2026 14:29:42 +0200 Subject: [PATCH] Rebuilded sitemap generator --- dist/modules/seo/buildSitemapXml.d.ts | 30 ++++----- dist/modules/seo/buildSitemapXml.js | 41 ++++++------ dist/modules/seo/buildSitemapXml.js.map | 2 +- docs/seo.md | 38 ++++++----- src/modules/seo/assets/sitemap.xsl | 85 ------------------------- src/modules/seo/buildSitemapXml.ts | 43 +++++-------- 6 files changed, 70 insertions(+), 169 deletions(-) delete mode 100644 src/modules/seo/assets/sitemap.xsl diff --git a/dist/modules/seo/buildSitemapXml.d.ts b/dist/modules/seo/buildSitemapXml.d.ts index 970b389..ed4e5ed 100644 --- a/dist/modules/seo/buildSitemapXml.d.ts +++ b/dist/modules/seo/buildSitemapXml.d.ts @@ -1,33 +1,31 @@ import type { SitemapEntry } from './buildSitemapEntries.js'; /** - * 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 . 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 + * 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 points browsers at it; crawlers - * ignore it and read the XML. Include hreflang alternates as . + * 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. + * (this route OR the Next MetadataRoute). Two sitemaps confuse crawlers. */ -export declare function buildSitemapXml(entries: SitemapEntry[], opts?: { - stylesheetUrl?: string; -}): string; +export declare function buildSitemapXml(entries: SitemapEntry[]): string; diff --git a/dist/modules/seo/buildSitemapXml.js b/dist/modules/seo/buildSitemapXml.js index 5ef5640..6b205fc 100644 --- a/dist/modules/seo/buildSitemapXml.js +++ b/dist/modules/seo/buildSitemapXml.js @@ -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 . 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 + * 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 points browsers at it; crawlers - * ignore it and read the XML. Include hreflang alternates as . + * 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, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); 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(` ${esc(iso)}`); } - if (e.changeFrequency) parts.push(` ${e.changeFrequency}`); - if (typeof e.priority === 'number') parts.push(` ${e.priority}`); - // hreflang alternates + if (e.changeFrequency) { + parts.push(` ${e.changeFrequency}`); + } + if (typeof e.priority === 'number') { + parts.push(` ${e.priority}`); + } const alternates = e.alternates?.languages; if (alternates) { for (const [lang, href] of Object.entries(alternates)){ @@ -50,8 +52,7 @@ } return ` \n${parts.join('\n')}\n `; }).join('\n'); - const stylesheet = stylesheetUrl ? `\n` : ''; - return `\n` + stylesheet + `\n` + urls + `\n`; + return `\n` + `\n` + urls + `\n`; } //# sourceMappingURL=buildSitemapXml.js.map \ No newline at end of file diff --git a/dist/modules/seo/buildSitemapXml.js.map b/dist/modules/seo/buildSitemapXml.js.map index 73e23f7..b366dc5 100644 --- a/dist/modules/seo/buildSitemapXml.js.map +++ b/dist/modules/seo/buildSitemapXml.js.map @@ -1 +1 @@ -{"version":3,"sources":["../../../src/modules/seo/buildSitemapXml.ts"],"sourcesContent":["import type { SitemapEntry } from './buildSitemapEntries.js'\n\n/**\n * Serializes sitemap entries to an XML STRING with an XSL stylesheet reference,\n * so /sitemap.xml renders as a readable table in the browser (not raw XML) while\n * staying a valid sitemap for crawlers.\n *\n * Why this exists alongside the Next MetadataRoute sitemap: Next's app/sitemap.ts\n * (returning SitemapEntry[]) does NOT let you inject . To get\n * the styled table, serve a custom route that returns this string instead:\n *\n * // app/sitemap.xml/route.ts\n * import { buildSitemapXml } from '@intecion/ipal-kit'\n * import { sitemap } from '@/lib/content' // your entries source\n * export const dynamic = 'force-dynamic'\n * export async function GET() {\n * const entries = await sitemap()\n * const xml = buildSitemapXml(entries, { stylesheetUrl: '/sitemap.xsl' })\n * return new Response(xml, {\n * headers: { 'Content-Type': 'application/xml; charset=utf-8' },\n * })\n * }\n *\n * Put sitemap.xsl in the project's /public (copy from the plugin's assets, or\n * serve it from a route). The points browsers at it; crawlers\n * ignore it and read the XML. Include hreflang alternates as .\n *\n * NOTE: if you use this custom route, DON'T also keep app/sitemap.ts — pick one\n * (the styled route OR the Next MetadataRoute). Two sitemaps at different paths\n * confuse crawlers.\n */\nexport function buildSitemapXml(\n entries: SitemapEntry[],\n opts: { stylesheetUrl?: string } = {},\n): string {\n const { stylesheetUrl } = opts\n\n const esc = (s: string): string =>\n s\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n\n const urls = entries\n .map((e) => {\n const parts = [` ${esc(e.url)}`]\n if (e.lastModified) {\n const iso =\n e.lastModified instanceof Date ? e.lastModified.toISOString() : String(e.lastModified)\n parts.push(` ${esc(iso)}`)\n }\n if (e.changeFrequency) parts.push(` ${e.changeFrequency}`)\n if (typeof e.priority === 'number') parts.push(` ${e.priority}`)\n // hreflang alternates\n const alternates = e.alternates?.languages\n if (alternates) {\n for (const [lang, href] of Object.entries(alternates)) {\n if (typeof href === 'string') {\n parts.push(\n ` `,\n )\n }\n }\n }\n return ` \\n${parts.join('\\n')}\\n `\n })\n .join('\\n')\n\n const stylesheet = stylesheetUrl\n ? `\\n`\n : ''\n\n return (\n `\\n` +\n stylesheet +\n `\\n` +\n urls +\n `\\n`\n )\n}\n"],"names":["buildSitemapXml","entries","opts","stylesheetUrl","esc","s","replace","urls","map","e","parts","url","lastModified","iso","Date","toISOString","String","push","changeFrequency","priority","alternates","languages","lang","href","Object","join","stylesheet"],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,SAASA,gBACdC,OAAuB,EACvBC,OAAmC,CAAC,CAAC;IAErC,MAAM,EAAEC,aAAa,EAAE,GAAGD;IAE1B,MAAME,MAAM,CAACC,IACXA,EACGC,OAAO,CAAC,MAAM,SACdA,OAAO,CAAC,MAAM,QACdA,OAAO,CAAC,MAAM,QACdA,OAAO,CAAC,MAAM,UACdA,OAAO,CAAC,MAAM;IAEnB,MAAMC,OAAON,QACVO,GAAG,CAAC,CAACC;QACJ,MAAMC,QAAQ;YAAC,CAAC,SAAS,EAAEN,IAAIK,EAAEE,GAAG,EAAE,MAAM,CAAC;SAAC;QAC9C,IAAIF,EAAEG,YAAY,EAAE;YAClB,MAAMC,MACJJ,EAAEG,YAAY,YAAYE,OAAOL,EAAEG,YAAY,CAACG,WAAW,KAAKC,OAAOP,EAAEG,YAAY;YACvFF,MAAMO,IAAI,CAAC,CAAC,aAAa,EAAEb,IAAIS,KAAK,UAAU,CAAC;QACjD;QACA,IAAIJ,EAAES,eAAe,EAAER,MAAMO,IAAI,CAAC,CAAC,gBAAgB,EAAER,EAAES,eAAe,CAAC,aAAa,CAAC;QACrF,IAAI,OAAOT,EAAEU,QAAQ,KAAK,UAAUT,MAAMO,IAAI,CAAC,CAAC,cAAc,EAAER,EAAEU,QAAQ,CAAC,WAAW,CAAC;QACvF,sBAAsB;QACtB,MAAMC,aAAaX,EAAEW,UAAU,EAAEC;QACjC,IAAID,YAAY;YACd,KAAK,MAAM,CAACE,MAAMC,KAAK,IAAIC,OAAOvB,OAAO,CAACmB,YAAa;gBACrD,IAAI,OAAOG,SAAS,UAAU;oBAC5Bb,MAAMO,IAAI,CACR,CAAC,0CAA0C,EAAEb,IAAIkB,MAAM,QAAQ,EAAElB,IAAImB,MAAM,GAAG,CAAC;gBAEnF;YACF;QACF;QACA,OAAO,CAAC,SAAS,EAAEb,MAAMe,IAAI,CAAC,MAAM,UAAU,CAAC;IACjD,GACCA,IAAI,CAAC;IAER,MAAMC,aAAavB,gBACf,CAAC,uCAAuC,EAAEC,IAAID,eAAe,KAAK,CAAC,GACnE;IAEJ,OACE,CAAC,wCAAwC,CAAC,GAC1CuB,aACA,CAAC,4DAA4D,CAAC,GAC9D,CAAC,6CAA6C,CAAC,GAC/CnB,OACA,CAAC,WAAW,CAAC;AAEjB"} \ No newline at end of file +{"version":3,"sources":["../../../src/modules/seo/buildSitemapXml.ts"],"sourcesContent":["import type { SitemapEntry } from './buildSitemapEntries.js'\n\n/**\n * Serializes sitemap entries to a clean, indented XML STRING — valid for crawlers\n * and readable in the browser as the native XML tree (indented, collapsible,\n * syntax-highlighted by the browser itself).\n *\n * NO XSLT stylesheet: browsers (Chrome et al.) are REMOVING XSLT support, so a\n * approach is a dead end — it shows a deprecation warning now\n * and will break soon. Instead we serve plain XML with the correct Content-Type;\n * the browser renders its built-in formatted XML view (the \"code tree\" look) with\n * no transformation needed.\n *\n * // app/sitemap.xml/route.ts\n * import { buildSitemapXml } from '@intecion/ipal-kit'\n * import { sitemap } from '@/lib/content'\n * export const dynamic = 'force-dynamic'\n * export async function GET() {\n * const entries = await sitemap()\n * const xml = buildSitemapXml(entries)\n * return new Response(xml, {\n * headers: { 'Content-Type': 'application/xml; charset=utf-8' },\n * })\n * }\n *\n * The indentation makes the raw XML pleasant to read; the browser's default XML\n * viewer adds the tree/fold UI. Crawlers read the XML as usual.\n *\n * NOTE: if you use this custom route, DON'T also keep app/sitemap.ts — pick one\n * (this route OR the Next MetadataRoute). Two sitemaps confuse crawlers.\n */\nexport function buildSitemapXml(entries: SitemapEntry[]): string {\n const esc = (s: string): string =>\n s\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n\n const urls = entries\n .map((e) => {\n const parts = [` ${esc(e.url)}`]\n if (e.lastModified) {\n const iso =\n e.lastModified instanceof Date ? e.lastModified.toISOString() : String(e.lastModified)\n parts.push(` ${esc(iso)}`)\n }\n if (e.changeFrequency) {parts.push(` ${e.changeFrequency}`)}\n if (typeof e.priority === 'number') {parts.push(` ${e.priority}`)}\n const alternates = e.alternates?.languages\n if (alternates) {\n for (const [lang, href] of Object.entries(alternates)) {\n if (typeof href === 'string') {\n parts.push(\n ` `,\n )\n }\n }\n }\n return ` \\n${parts.join('\\n')}\\n `\n })\n .join('\\n')\n\n return (\n `\\n` +\n `\\n` +\n urls +\n `\\n`\n )\n}\n"],"names":["buildSitemapXml","entries","esc","s","replace","urls","map","e","parts","url","lastModified","iso","Date","toISOString","String","push","changeFrequency","priority","alternates","languages","lang","href","Object","join"],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,SAASA,gBAAgBC,OAAuB;IACrD,MAAMC,MAAM,CAACC,IACXA,EACGC,OAAO,CAAC,MAAM,SACdA,OAAO,CAAC,MAAM,QACdA,OAAO,CAAC,MAAM,QACdA,OAAO,CAAC,MAAM,UACdA,OAAO,CAAC,MAAM;IAEnB,MAAMC,OAAOJ,QACVK,GAAG,CAAC,CAACC;QACJ,MAAMC,QAAQ;YAAC,CAAC,SAAS,EAAEN,IAAIK,EAAEE,GAAG,EAAE,MAAM,CAAC;SAAC;QAC9C,IAAIF,EAAEG,YAAY,EAAE;YAClB,MAAMC,MACJJ,EAAEG,YAAY,YAAYE,OAAOL,EAAEG,YAAY,CAACG,WAAW,KAAKC,OAAOP,EAAEG,YAAY;YACvFF,MAAMO,IAAI,CAAC,CAAC,aAAa,EAAEb,IAAIS,KAAK,UAAU,CAAC;QACjD;QACA,IAAIJ,EAAES,eAAe,EAAE;YAACR,MAAMO,IAAI,CAAC,CAAC,gBAAgB,EAAER,EAAES,eAAe,CAAC,aAAa,CAAC;QAAC;QACvF,IAAI,OAAOT,EAAEU,QAAQ,KAAK,UAAU;YAACT,MAAMO,IAAI,CAAC,CAAC,cAAc,EAAER,EAAEU,QAAQ,CAAC,WAAW,CAAC;QAAC;QACzF,MAAMC,aAAaX,EAAEW,UAAU,EAAEC;QACjC,IAAID,YAAY;YACd,KAAK,MAAM,CAACE,MAAMC,KAAK,IAAIC,OAAOrB,OAAO,CAACiB,YAAa;gBACrD,IAAI,OAAOG,SAAS,UAAU;oBAC5Bb,MAAMO,IAAI,CACR,CAAC,0CAA0C,EAAEb,IAAIkB,MAAM,QAAQ,EAAElB,IAAImB,MAAM,GAAG,CAAC;gBAEnF;YACF;QACF;QACA,OAAO,CAAC,SAAS,EAAEb,MAAMe,IAAI,CAAC,MAAM,UAAU,CAAC;IACjD,GACCA,IAAI,CAAC;IAER,OACE,CAAC,wCAAwC,CAAC,GAC1C,CAAC,4DAA4D,CAAC,GAC9D,CAAC,6CAA6C,CAAC,GAC/ClB,OACA,CAAC,WAAW,CAAC;AAEjB"} \ No newline at end of file diff --git a/docs/seo.md b/docs/seo.md index 5ed20fc..4e5010a 100644 --- a/docs/seo.md +++ b/docs/seo.md @@ -886,17 +886,17 @@ Generative Engine Optimization) i audytach „Agentic Browsing”. Wymaga wypełnionego siteDescription i sensownych meta.description stron (inaczej llms.txt będzie ubogi). -## Sitemap jako czytelna tabela (XSL stylesheet) +## Sitemap czytelny w przeglądarce (czysty XML) -Domyślny `/sitemap.xml` to surowy XML — Google go czyta, ale człowiek widzi -„ścianę tagów". Można ostylować przez XSL (przeglądarka renderuje tabelę), -zachowując poprawność dla crawlerów. +Domyślny `/sitemap.xml` (Next MetadataRoute) działa dla Google, ale w przeglądarce +bywa nieczytelny. Możesz serwować go jako **czysty, sformatowany XML** przez +`buildSitemapXml` — przeglądarka pokaże wbudowane drzewo XML (wcięcia, zwijanie, +kolorowanie składni), bez żadnej transformacji. -### Ograniczenie Next - -Standardowy `app/sitemap.ts` (Next MetadataRoute) **nie pozwala** wstrzyknąć -``. Żeby mieć styl, serwuj sitemap własnym route przez -`buildSitemapXml` (string XML z odwołaniem do XSL): +> **NIE używaj XSLT.** Przeglądarki (Chrome i in.) WYCOFUJĄ XSLT — arkusz +> `` pokazuje ostrzeżenie i wkrótce przestanie działać. +> Rozwiązanie: serwuj czysty XML z poprawnym Content-Type; przeglądarka +> renderuje swój natywny widok drzewa XML sama. ```ts // app/sitemap.xml/route.ts (zamiast app/sitemap.ts) @@ -907,24 +907,22 @@ 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' }, }) } ``` -### Plik XSL w /public - -Skopiuj `sitemap.xsl` (z pluginu: `node_modules/@intecion/ipal-kit/dist/modules/seo/assets/sitemap.xsl`) -do `public/sitemap.xsl` w projekcie. Zawiera responsywną tabelę (numer, URL, -data, języki) z dark mode. `stylesheetUrl: '/sitemap.xsl'` wskazuje na niego. +`buildSitemapXml` zwraca wcięty XML (czytelny w surowej postaci), a przeglądarka +dokłada widok drzewa. Zawiera hreflang jako ``. Crawlery czytają XML +normalnie. ### WAŻNE — jeden sitemap, nie dwa -Jeśli używasz route `app/sitemap.xml/route.ts` (styled), **USUŃ** `app/sitemap.ts` -(MetadataRoute). Dwa sitemapy pod różnymi ścieżkami mylą crawlery. Wybierz jeden: -- **styled** (`sitemap.xml/route.ts` + buildSitemapXml + XSL) — ładna tabela -- **prosty** (`sitemap.ts` + reeksport z lib/content) — bez stylu, mniej kodu +Jeśli używasz `app/sitemap.xml/route.ts`, USUŃ `app/sitemap.ts` (MetadataRoute). +Dwa sitemapy pod różnymi ścieżkami mylą crawlery. Wybierz jeden: +- **route.ts + buildSitemapXml** — czytelny XML w przeglądarce +- **sitemap.ts** (reeksport z lib/content) — mniej kodu, mniej czytelny w przeglądarce -Styl to kosmetyka (Google czyta oba tak samo) — rób, jeśli klient/audyt tego chce. \ No newline at end of file +Oba tak samo dobre dla Google — to kwestia czytelności dla człowieka, nie SEO. \ No newline at end of file diff --git a/src/modules/seo/assets/sitemap.xsl b/src/modules/seo/assets/sitemap.xsl deleted file mode 100644 index 1dcca51..0000000 --- a/src/modules/seo/assets/sitemap.xsl +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - Sitemap - - - -
-

Mapa witryny (Sitemap)

-

- Ten plik jest odczytywany przez wyszukiwarki. - Adresów: -

- - - - - - - - - - - - - - - - - - - -
#Adres URLOstatnia zmianaJęzyki
- - - - - , - -
-
- - -
-
diff --git a/src/modules/seo/buildSitemapXml.ts b/src/modules/seo/buildSitemapXml.ts index 75205af..e3d5acb 100644 --- a/src/modules/seo/buildSitemapXml.ts +++ b/src/modules/seo/buildSitemapXml.ts @@ -1,40 +1,35 @@ import type { SitemapEntry } from './buildSitemapEntries.js' /** - * 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 . 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 + * 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 points browsers at it; crawlers - * ignore it and read the XML. Include hreflang alternates as . + * 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. + * (this route OR the Next MetadataRoute). Two sitemaps confuse crawlers. */ -export function buildSitemapXml( - entries: SitemapEntry[], - opts: { stylesheetUrl?: string } = {}, -): string { - const { stylesheetUrl } = opts - +export function buildSitemapXml(entries: SitemapEntry[]): string { const esc = (s: string): string => s .replace(/&/g, '&') @@ -51,9 +46,8 @@ export function buildSitemapXml( e.lastModified instanceof Date ? e.lastModified.toISOString() : String(e.lastModified) parts.push(` ${esc(iso)}`) } - if (e.changeFrequency) parts.push(` ${e.changeFrequency}`) - if (typeof e.priority === 'number') parts.push(` ${e.priority}`) - // hreflang alternates + if (e.changeFrequency) {parts.push(` ${e.changeFrequency}`)} + if (typeof e.priority === 'number') {parts.push(` ${e.priority}`)} const alternates = e.alternates?.languages if (alternates) { for (const [lang, href] of Object.entries(alternates)) { @@ -68,13 +62,8 @@ export function buildSitemapXml( }) .join('\n') - const stylesheet = stylesheetUrl - ? `\n` - : '' - return ( `\n` + - stylesheet + `\n` + urls +