Rebuilded sitemap generator
This commit is contained in:
+14
-16
@@ -1,33 +1,31 @@
|
|||||||
import type { SitemapEntry } from './buildSitemapEntries.js';
|
import type { SitemapEntry } from './buildSitemapEntries.js';
|
||||||
/**
|
/**
|
||||||
* Serializes sitemap entries to an XML STRING with an XSL stylesheet reference,
|
* Serializes sitemap entries to a clean, indented XML STRING — valid for crawlers
|
||||||
* so /sitemap.xml renders as a readable table in the browser (not raw XML) while
|
* and readable in the browser as the native XML tree (indented, collapsible,
|
||||||
* staying a valid sitemap for crawlers.
|
* syntax-highlighted by the browser itself).
|
||||||
*
|
*
|
||||||
* Why this exists alongside the Next MetadataRoute sitemap: Next's app/sitemap.ts
|
* NO XSLT stylesheet: browsers (Chrome et al.) are REMOVING XSLT support, so a
|
||||||
* (returning SitemapEntry[]) does NOT let you inject <?xml-stylesheet?>. To get
|
* <?xml-stylesheet?> approach is a dead end — it shows a deprecation warning now
|
||||||
* the styled table, serve a custom route that returns this string instead:
|
* 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
|
* // app/sitemap.xml/route.ts
|
||||||
* import { buildSitemapXml } from '@intecion/ipal-kit'
|
* 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 const dynamic = 'force-dynamic'
|
||||||
* export async function GET() {
|
* export async function GET() {
|
||||||
* const entries = await sitemap()
|
* const entries = await sitemap()
|
||||||
* const xml = buildSitemapXml(entries, { stylesheetUrl: '/sitemap.xsl' })
|
* const xml = buildSitemapXml(entries)
|
||||||
* return new Response(xml, {
|
* return new Response(xml, {
|
||||||
* headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
* headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
||||||
* })
|
* })
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* Put sitemap.xsl in the project's /public (copy from the plugin's assets, or
|
* The indentation makes the raw XML pleasant to read; the browser's default XML
|
||||||
* serve it from a route). The <?xml-stylesheet?> points browsers at it; crawlers
|
* viewer adds the tree/fold UI. Crawlers read the XML as usual.
|
||||||
* ignore it and read the XML. Include hreflang alternates as <xhtml:link>.
|
|
||||||
*
|
*
|
||||||
* NOTE: if you use this custom route, DON'T also keep app/sitemap.ts — pick one
|
* 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
|
* (this route OR the Next MetadataRoute). Two sitemaps confuse crawlers.
|
||||||
* confuse crawlers.
|
|
||||||
*/
|
*/
|
||||||
export declare function buildSitemapXml(entries: SitemapEntry[], opts?: {
|
export declare function buildSitemapXml(entries: SitemapEntry[]): string;
|
||||||
stylesheetUrl?: string;
|
|
||||||
}): string;
|
|
||||||
|
|||||||
Vendored
+21
-20
@@ -1,33 +1,32 @@
|
|||||||
/**
|
/**
|
||||||
* Serializes sitemap entries to an XML STRING with an XSL stylesheet reference,
|
* Serializes sitemap entries to a clean, indented XML STRING — valid for crawlers
|
||||||
* so /sitemap.xml renders as a readable table in the browser (not raw XML) while
|
* and readable in the browser as the native XML tree (indented, collapsible,
|
||||||
* staying a valid sitemap for crawlers.
|
* syntax-highlighted by the browser itself).
|
||||||
*
|
*
|
||||||
* Why this exists alongside the Next MetadataRoute sitemap: Next's app/sitemap.ts
|
* NO XSLT stylesheet: browsers (Chrome et al.) are REMOVING XSLT support, so a
|
||||||
* (returning SitemapEntry[]) does NOT let you inject <?xml-stylesheet?>. To get
|
* <?xml-stylesheet?> approach is a dead end — it shows a deprecation warning now
|
||||||
* the styled table, serve a custom route that returns this string instead:
|
* 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
|
* // app/sitemap.xml/route.ts
|
||||||
* import { buildSitemapXml } from '@intecion/ipal-kit'
|
* 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 const dynamic = 'force-dynamic'
|
||||||
* export async function GET() {
|
* export async function GET() {
|
||||||
* const entries = await sitemap()
|
* const entries = await sitemap()
|
||||||
* const xml = buildSitemapXml(entries, { stylesheetUrl: '/sitemap.xsl' })
|
* const xml = buildSitemapXml(entries)
|
||||||
* return new Response(xml, {
|
* return new Response(xml, {
|
||||||
* headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
* headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
||||||
* })
|
* })
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* Put sitemap.xsl in the project's /public (copy from the plugin's assets, or
|
* The indentation makes the raw XML pleasant to read; the browser's default XML
|
||||||
* serve it from a route). The <?xml-stylesheet?> points browsers at it; crawlers
|
* viewer adds the tree/fold UI. Crawlers read the XML as usual.
|
||||||
* ignore it and read the XML. Include hreflang alternates as <xhtml:link>.
|
|
||||||
*
|
*
|
||||||
* NOTE: if you use this custom route, DON'T also keep app/sitemap.ts — pick one
|
* 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
|
* (this route OR the Next MetadataRoute). Two sitemaps confuse crawlers.
|
||||||
* confuse crawlers.
|
*/ export function buildSitemapXml(entries) {
|
||||||
*/ export function buildSitemapXml(entries, opts = {}) {
|
|
||||||
const { stylesheetUrl } = opts;
|
|
||||||
const esc = (s)=>s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
const esc = (s)=>s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||||
const urls = entries.map((e)=>{
|
const urls = entries.map((e)=>{
|
||||||
const parts = [
|
const parts = [
|
||||||
@@ -37,9 +36,12 @@
|
|||||||
const iso = e.lastModified instanceof Date ? e.lastModified.toISOString() : String(e.lastModified);
|
const iso = e.lastModified instanceof Date ? e.lastModified.toISOString() : String(e.lastModified);
|
||||||
parts.push(` <lastmod>${esc(iso)}</lastmod>`);
|
parts.push(` <lastmod>${esc(iso)}</lastmod>`);
|
||||||
}
|
}
|
||||||
if (e.changeFrequency) parts.push(` <changefreq>${e.changeFrequency}</changefreq>`);
|
if (e.changeFrequency) {
|
||||||
if (typeof e.priority === 'number') parts.push(` <priority>${e.priority}</priority>`);
|
parts.push(` <changefreq>${e.changeFrequency}</changefreq>`);
|
||||||
// hreflang alternates
|
}
|
||||||
|
if (typeof e.priority === 'number') {
|
||||||
|
parts.push(` <priority>${e.priority}</priority>`);
|
||||||
|
}
|
||||||
const alternates = e.alternates?.languages;
|
const alternates = e.alternates?.languages;
|
||||||
if (alternates) {
|
if (alternates) {
|
||||||
for (const [lang, href] of Object.entries(alternates)){
|
for (const [lang, href] of Object.entries(alternates)){
|
||||||
@@ -50,8 +52,7 @@
|
|||||||
}
|
}
|
||||||
return ` <url>\n${parts.join('\n')}\n </url>`;
|
return ` <url>\n${parts.join('\n')}\n </url>`;
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
const stylesheet = stylesheetUrl ? `<?xml-stylesheet type="text/xsl" href="${esc(stylesheetUrl)}"?>\n` : '';
|
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>`;
|
||||||
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>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//# sourceMappingURL=buildSitemapXml.js.map
|
//# sourceMappingURL=buildSitemapXml.js.map
|
||||||
+1
-1
@@ -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 <?xml-stylesheet?>. 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 <?xml-stylesheet?> points browsers at it; crawlers\n * ignore it and read the XML. Include hreflang alternates as <xhtml:link>.\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 .replace(/'/g, ''')\n\n const urls = entries\n .map((e) => {\n const parts = [` <loc>${esc(e.url)}</loc>`]\n if (e.lastModified) {\n const iso =\n e.lastModified instanceof Date ? e.lastModified.toISOString() : String(e.lastModified)\n parts.push(` <lastmod>${esc(iso)}</lastmod>`)\n }\n if (e.changeFrequency) parts.push(` <changefreq>${e.changeFrequency}</changefreq>`)\n if (typeof e.priority === 'number') parts.push(` <priority>${e.priority}</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 ` <xhtml:link rel=\"alternate\" hreflang=\"${esc(lang)}\" href=\"${esc(href)}\"/>`,\n )\n }\n }\n }\n return ` <url>\\n${parts.join('\\n')}\\n </url>`\n })\n .join('\\n')\n\n const stylesheet = stylesheetUrl\n ? `<?xml-stylesheet type=\"text/xsl\" href=\"${esc(stylesheetUrl)}\"?>\\n`\n : ''\n\n return (\n `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n` +\n stylesheet +\n `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" ` +\n `xmlns:xhtml=\"http://www.w3.org/1999/xhtml\">\\n` +\n urls +\n `\\n</urlset>`\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"}
|
{"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 * <?xml-stylesheet?> 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 .replace(/'/g, ''')\n\n const urls = entries\n .map((e) => {\n const parts = [` <loc>${esc(e.url)}</loc>`]\n if (e.lastModified) {\n const iso =\n e.lastModified instanceof Date ? e.lastModified.toISOString() : String(e.lastModified)\n parts.push(` <lastmod>${esc(iso)}</lastmod>`)\n }\n if (e.changeFrequency) {parts.push(` <changefreq>${e.changeFrequency}</changefreq>`)}\n if (typeof e.priority === 'number') {parts.push(` <priority>${e.priority}</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 ` <xhtml:link rel=\"alternate\" hreflang=\"${esc(lang)}\" href=\"${esc(href)}\"/>`,\n )\n }\n }\n }\n return ` <url>\\n${parts.join('\\n')}\\n </url>`\n })\n .join('\\n')\n\n return (\n `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n` +\n `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" ` +\n `xmlns:xhtml=\"http://www.w3.org/1999/xhtml\">\\n` +\n urls +\n `\\n</urlset>`\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"}
|
||||||
+18
-20
@@ -886,17 +886,17 @@ Generative Engine Optimization) i audytach „Agentic Browsing”.
|
|||||||
Wymaga wypełnionego siteDescription i sensownych meta.description stron
|
Wymaga wypełnionego siteDescription i sensownych meta.description stron
|
||||||
(inaczej llms.txt będzie ubogi).
|
(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
|
Domyślny `/sitemap.xml` (Next MetadataRoute) działa dla Google, ale w przeglądarce
|
||||||
„ścianę tagów". Można ostylować przez XSL (przeglądarka renderuje tabelę),
|
bywa nieczytelny. Możesz serwować go jako **czysty, sformatowany XML** przez
|
||||||
zachowując poprawność dla crawlerów.
|
`buildSitemapXml` — przeglądarka pokaże wbudowane drzewo XML (wcięcia, zwijanie,
|
||||||
|
kolorowanie składni), bez żadnej transformacji.
|
||||||
|
|
||||||
### Ograniczenie Next
|
> **NIE używaj XSLT.** Przeglądarki (Chrome i in.) WYCOFUJĄ XSLT — arkusz
|
||||||
|
> `<?xml-stylesheet?>` pokazuje ostrzeżenie i wkrótce przestanie działać.
|
||||||
Standardowy `app/sitemap.ts` (Next MetadataRoute) **nie pozwala** wstrzyknąć
|
> Rozwiązanie: serwuj czysty XML z poprawnym Content-Type; przeglądarka
|
||||||
`<?xml-stylesheet?>`. Żeby mieć styl, serwuj sitemap własnym route przez
|
> renderuje swój natywny widok drzewa XML sama.
|
||||||
`buildSitemapXml` (string XML z odwołaniem do XSL):
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// app/sitemap.xml/route.ts (zamiast app/sitemap.ts)
|
// app/sitemap.xml/route.ts (zamiast app/sitemap.ts)
|
||||||
@@ -907,24 +907,22 @@ export const dynamic = 'force-dynamic'
|
|||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const entries = await sitemap()
|
const entries = await sitemap()
|
||||||
const xml = buildSitemapXml(entries, { stylesheetUrl: '/sitemap.xsl' })
|
const xml = buildSitemapXml(entries)
|
||||||
return new Response(xml, {
|
return new Response(xml, {
|
||||||
headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Plik XSL w /public
|
`buildSitemapXml` zwraca wcięty XML (czytelny w surowej postaci), a przeglądarka
|
||||||
|
dokłada widok drzewa. Zawiera hreflang jako `<xhtml:link>`. Crawlery czytają XML
|
||||||
Skopiuj `sitemap.xsl` (z pluginu: `node_modules/@intecion/ipal-kit/dist/modules/seo/assets/sitemap.xsl`)
|
normalnie.
|
||||||
do `public/sitemap.xsl` w projekcie. Zawiera responsywną tabelę (numer, URL,
|
|
||||||
data, języki) z dark mode. `stylesheetUrl: '/sitemap.xsl'` wskazuje na niego.
|
|
||||||
|
|
||||||
### WAŻNE — jeden sitemap, nie dwa
|
### WAŻNE — jeden sitemap, nie dwa
|
||||||
|
|
||||||
Jeśli używasz route `app/sitemap.xml/route.ts` (styled), **USUŃ** `app/sitemap.ts`
|
Jeśli używasz `app/sitemap.xml/route.ts`, USUŃ `app/sitemap.ts` (MetadataRoute).
|
||||||
(MetadataRoute). Dwa sitemapy pod różnymi ścieżkami mylą crawlery. Wybierz jeden:
|
Dwa sitemapy pod różnymi ścieżkami mylą crawlery. Wybierz jeden:
|
||||||
- **styled** (`sitemap.xml/route.ts` + buildSitemapXml + XSL) — ładna tabela
|
- **route.ts + buildSitemapXml** — czytelny XML w przeglądarce
|
||||||
- **prosty** (`sitemap.ts` + reeksport z lib/content) — bez stylu, mniej kodu
|
- **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.
|
Oba tak samo dobre dla Google — to kwestia czytelności dla człowieka, nie SEO.
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<xsl:stylesheet version="1.0"
|
|
||||||
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
|
||||||
xmlns:s="http://www.sitemaps.org/schemas/sitemap/0.9"
|
|
||||||
xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
|
||||||
<xsl:output method="html" encoding="UTF-8" indent="yes"/>
|
|
||||||
<xsl:template match="/">
|
|
||||||
<html lang="pl">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8"/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
||||||
<title>Sitemap</title>
|
|
||||||
<style>
|
|
||||||
:root { color-scheme: light dark; }
|
|
||||||
* { box-sizing: border-box; }
|
|
||||||
body {
|
|
||||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
|
||||||
margin: 0; padding: 2rem 1rem; line-height: 1.5;
|
|
||||||
color: #1a1a1a; background: #f7f7f8;
|
|
||||||
}
|
|
||||||
.wrap { max-width: 1100px; margin: 0 auto; }
|
|
||||||
h1 { font-size: 1.5rem; margin: 0 0 .25rem; }
|
|
||||||
.meta { color: #666; font-size: .9rem; margin-bottom: 1.5rem; }
|
|
||||||
.count { font-weight: 600; }
|
|
||||||
table { width: 100%; border-collapse: collapse; background: #fff;
|
|
||||||
border-radius: .5rem; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
|
|
||||||
th, td { text-align: left; padding: .65rem .9rem; border-bottom: 1px solid #eee;
|
|
||||||
font-size: .9rem; }
|
|
||||||
th { background: #fafafa; font-weight: 600; color: #444;
|
|
||||||
position: sticky; top: 0; }
|
|
||||||
tr:last-child td { border-bottom: none; }
|
|
||||||
tr:hover td { background: #f5f8ff; }
|
|
||||||
a { color: #2563eb; text-decoration: none; word-break: break-all; }
|
|
||||||
a:hover { text-decoration: underline; }
|
|
||||||
.num { color: #999; width: 3rem; text-align: right; font-variant-numeric: tabular-nums; }
|
|
||||||
.langs { color: #666; font-size: .8rem; }
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
body { color: #e5e5e5; background: #18181b; }
|
|
||||||
table { background: #232327; box-shadow: none; }
|
|
||||||
th { background: #27272a; color: #ccc; }
|
|
||||||
th, td { border-color: #333; }
|
|
||||||
tr:hover td { background: #1e293b; }
|
|
||||||
.meta, .num, .langs { color: #888; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="wrap">
|
|
||||||
<h1>Mapa witryny (Sitemap)</h1>
|
|
||||||
<p class="meta">
|
|
||||||
Ten plik jest odczytywany przez wyszukiwarki.
|
|
||||||
Adresów: <span class="count"><xsl:value-of select="count(s:urlset/s:url)"/></span>
|
|
||||||
</p>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="num">#</th>
|
|
||||||
<th>Adres URL</th>
|
|
||||||
<th>Ostatnia zmiana</th>
|
|
||||||
<th>Języki</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<xsl:for-each select="s:urlset/s:url">
|
|
||||||
<tr>
|
|
||||||
<td class="num"><xsl:value-of select="position()"/></td>
|
|
||||||
<td>
|
|
||||||
<a href="{s:loc}"><xsl:value-of select="s:loc"/></a>
|
|
||||||
</td>
|
|
||||||
<td><xsl:value-of select="s:lastmod"/></td>
|
|
||||||
<td class="langs">
|
|
||||||
<xsl:for-each select="xhtml:link">
|
|
||||||
<xsl:value-of select="@hreflang"/>
|
|
||||||
<xsl:if test="position() != last()"><xsl:text>, </xsl:text></xsl:if>
|
|
||||||
</xsl:for-each>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</xsl:for-each>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
</xsl:template>
|
|
||||||
</xsl:stylesheet>
|
|
||||||
@@ -1,40 +1,35 @@
|
|||||||
import type { SitemapEntry } from './buildSitemapEntries.js'
|
import type { SitemapEntry } from './buildSitemapEntries.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializes sitemap entries to an XML STRING with an XSL stylesheet reference,
|
* Serializes sitemap entries to a clean, indented XML STRING — valid for crawlers
|
||||||
* so /sitemap.xml renders as a readable table in the browser (not raw XML) while
|
* and readable in the browser as the native XML tree (indented, collapsible,
|
||||||
* staying a valid sitemap for crawlers.
|
* syntax-highlighted by the browser itself).
|
||||||
*
|
*
|
||||||
* Why this exists alongside the Next MetadataRoute sitemap: Next's app/sitemap.ts
|
* NO XSLT stylesheet: browsers (Chrome et al.) are REMOVING XSLT support, so a
|
||||||
* (returning SitemapEntry[]) does NOT let you inject <?xml-stylesheet?>. To get
|
* <?xml-stylesheet?> approach is a dead end — it shows a deprecation warning now
|
||||||
* the styled table, serve a custom route that returns this string instead:
|
* 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
|
* // app/sitemap.xml/route.ts
|
||||||
* import { buildSitemapXml } from '@intecion/ipal-kit'
|
* 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 const dynamic = 'force-dynamic'
|
||||||
* export async function GET() {
|
* export async function GET() {
|
||||||
* const entries = await sitemap()
|
* const entries = await sitemap()
|
||||||
* const xml = buildSitemapXml(entries, { stylesheetUrl: '/sitemap.xsl' })
|
* const xml = buildSitemapXml(entries)
|
||||||
* return new Response(xml, {
|
* return new Response(xml, {
|
||||||
* headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
* headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
||||||
* })
|
* })
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* Put sitemap.xsl in the project's /public (copy from the plugin's assets, or
|
* The indentation makes the raw XML pleasant to read; the browser's default XML
|
||||||
* serve it from a route). The <?xml-stylesheet?> points browsers at it; crawlers
|
* viewer adds the tree/fold UI. Crawlers read the XML as usual.
|
||||||
* ignore it and read the XML. Include hreflang alternates as <xhtml:link>.
|
|
||||||
*
|
*
|
||||||
* NOTE: if you use this custom route, DON'T also keep app/sitemap.ts — pick one
|
* 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
|
* (this route OR the Next MetadataRoute). Two sitemaps confuse crawlers.
|
||||||
* confuse crawlers.
|
|
||||||
*/
|
*/
|
||||||
export function buildSitemapXml(
|
export function buildSitemapXml(entries: SitemapEntry[]): string {
|
||||||
entries: SitemapEntry[],
|
|
||||||
opts: { stylesheetUrl?: string } = {},
|
|
||||||
): string {
|
|
||||||
const { stylesheetUrl } = opts
|
|
||||||
|
|
||||||
const esc = (s: string): string =>
|
const esc = (s: string): string =>
|
||||||
s
|
s
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
@@ -51,9 +46,8 @@ export function buildSitemapXml(
|
|||||||
e.lastModified instanceof Date ? e.lastModified.toISOString() : String(e.lastModified)
|
e.lastModified instanceof Date ? e.lastModified.toISOString() : String(e.lastModified)
|
||||||
parts.push(` <lastmod>${esc(iso)}</lastmod>`)
|
parts.push(` <lastmod>${esc(iso)}</lastmod>`)
|
||||||
}
|
}
|
||||||
if (e.changeFrequency) parts.push(` <changefreq>${e.changeFrequency}</changefreq>`)
|
if (e.changeFrequency) {parts.push(` <changefreq>${e.changeFrequency}</changefreq>`)}
|
||||||
if (typeof e.priority === 'number') parts.push(` <priority>${e.priority}</priority>`)
|
if (typeof e.priority === 'number') {parts.push(` <priority>${e.priority}</priority>`)}
|
||||||
// hreflang alternates
|
|
||||||
const alternates = e.alternates?.languages
|
const alternates = e.alternates?.languages
|
||||||
if (alternates) {
|
if (alternates) {
|
||||||
for (const [lang, href] of Object.entries(alternates)) {
|
for (const [lang, href] of Object.entries(alternates)) {
|
||||||
@@ -68,13 +62,8 @@ export function buildSitemapXml(
|
|||||||
})
|
})
|
||||||
.join('\n')
|
.join('\n')
|
||||||
|
|
||||||
const stylesheet = stylesheetUrl
|
|
||||||
? `<?xml-stylesheet type="text/xsl" href="${esc(stylesheetUrl)}"?>\n`
|
|
||||||
: ''
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
||||||
stylesheet +
|
|
||||||
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" ` +
|
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" ` +
|
||||||
`xmlns:xhtml="http://www.w3.org/1999/xhtml">\n` +
|
`xmlns:xhtml="http://www.w3.org/1999/xhtml">\n` +
|
||||||
urls +
|
urls +
|
||||||
|
|||||||
Reference in New Issue
Block a user