Added styled Xml

This commit is contained in:
2026-09-19 22:24:36 +02:00
parent e03dd8c5a6
commit 3d350bd9e5
14 changed files with 311 additions and 3 deletions
+85
View File
@@ -0,0 +1,85 @@
<?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>
+83
View File
@@ -0,0 +1,83 @@
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.
*
* 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:
*
* // app/sitemap.xml/route.ts
* import { buildSitemapXml } from '@intecion/ipal-kit'
* import { sitemap } from '@/lib/content' // your entries source
* export const dynamic = 'force-dynamic'
* export async function GET() {
* const entries = await sitemap()
* const xml = buildSitemapXml(entries, { stylesheetUrl: '/sitemap.xsl' })
* 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>.
*
* 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: SitemapEntry[],
opts: { stylesheetUrl?: string } = {},
): string {
const { stylesheetUrl } = opts
const esc = (s: string): string =>
s
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
const urls = entries
.map((e) => {
const parts = [` <loc>${esc(e.url)}</loc>`]
if (e.lastModified) {
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
const alternates = e.alternates?.languages
if (alternates) {
for (const [lang, href] of Object.entries(alternates)) {
if (typeof href === 'string') {
parts.push(
` <xhtml:link rel="alternate" hreflang="${esc(lang)}" href="${esc(href)}"/>`,
)
}
}
}
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>`
)
}
+1
View File
@@ -14,6 +14,7 @@ export type { RobotsRules } from './buildRobots.js'
export { buildServiceJsonLd } from './buildServiceJsonLd.js'
export { buildSitemapEntries } from './buildSitemapEntries.js'
export type { SitemapEntry } from './buildSitemapEntries.js'
export { buildSitemapXml } from './buildSitemapXml.js'
export { buildSiteNavigationJsonLd } from './buildSiteNavigationJsonLd.js'
export { buildWebSiteJsonLd } from './buildWebSiteJsonLd.js'
export { composeTitle } from './composeTitle.js'