sitemap
This commit is contained in:
+24
-25
@@ -256,7 +256,9 @@ function sitemapChangefreq(urlPath) {
|
||||
|
||||
/**
|
||||
* Build a Google Search–compatible sitemap 0.9 document from dist HTML pages.
|
||||
* Strict sitemaps.org protocol only (loc, lastmod, changefreq, priority).
|
||||
* @see https://www.sitemaps.org/protocol.html
|
||||
* @see https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap
|
||||
*/
|
||||
function buildSitemapXml() {
|
||||
const pages = walkHtmlFiles(DIST)
|
||||
@@ -268,64 +270,61 @@ function buildSitemapXml() {
|
||||
const excludeKey = urlPath.replace(/\/+$/, '') || '/'
|
||||
if (SITEMAP_EXCLUDE.has(excludeKey) || SITEMAP_EXCLUDE.has(urlPath)) continue
|
||||
|
||||
const lastmod = page.mtime.toISOString().slice(0, 10) // W3C date (YYYY-MM-DD)
|
||||
// W3C Datetime (date only is accepted by Google)
|
||||
const lastmod = page.mtime.toISOString().slice(0, 10)
|
||||
const existing = byPath.get(urlPath)
|
||||
if (existing && existing.lastmod >= lastmod) continue
|
||||
|
||||
// Absolute HTTPS loc only — required by the protocol
|
||||
const loc = SITE_ORIGIN + (urlPath === '/' ? '/' : urlPath)
|
||||
byPath.set(urlPath, {
|
||||
loc: SITE_ORIGIN + (urlPath === '/' ? '/' : urlPath),
|
||||
loc,
|
||||
lastmod,
|
||||
changefreq: sitemapChangefreq(urlPath),
|
||||
priority: sitemapPriority(urlPath),
|
||||
})
|
||||
}
|
||||
|
||||
// Stable sort: home first, then path
|
||||
const entries = [...byPath.values()].sort((a, b) => {
|
||||
if (a.loc === `${SITE_ORIGIN}/`) return -1
|
||||
if (b.loc === `${SITE_ORIGIN}/`) return 1
|
||||
return a.loc.localeCompare(b.loc)
|
||||
})
|
||||
|
||||
const lines = [
|
||||
// One declaration + default namespace only (no xhtml extras — single-language site)
|
||||
const parts = [
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"',
|
||||
' xmlns:xhtml="http://www.w3.org/1999/xhtml">',
|
||||
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
||||
]
|
||||
|
||||
for (const e of entries) {
|
||||
lines.push(' <url>')
|
||||
lines.push(` <loc>${escapeXml(e.loc)}</loc>`)
|
||||
lines.push(` <lastmod>${escapeXml(e.lastmod)}</lastmod>`)
|
||||
lines.push(` <changefreq>${escapeXml(e.changefreq)}</changefreq>`)
|
||||
lines.push(` <priority>${escapeXml(e.priority)}</priority>`)
|
||||
// Self-referencing hreflang (single-language site)
|
||||
lines.push(
|
||||
` <xhtml:link rel="alternate" hreflang="en" href="${escapeXml(e.loc)}" />`
|
||||
parts.push(
|
||||
' <url>',
|
||||
` <loc>${escapeXml(e.loc)}</loc>`,
|
||||
` <lastmod>${escapeXml(e.lastmod)}</lastmod>`,
|
||||
` <changefreq>${escapeXml(e.changefreq)}</changefreq>`,
|
||||
` <priority>${escapeXml(e.priority)}</priority>`,
|
||||
' </url>'
|
||||
)
|
||||
lines.push(
|
||||
` <xhtml:link rel="alternate" hreflang="x-default" href="${escapeXml(e.loc)}" />`
|
||||
)
|
||||
lines.push(' </url>')
|
||||
}
|
||||
|
||||
lines.push('</urlset>')
|
||||
lines.push('')
|
||||
return lines.join('\n')
|
||||
parts.push('</urlset>')
|
||||
// Trailing newline for POSIX-friendly files
|
||||
return parts.join('\n') + '\n'
|
||||
}
|
||||
|
||||
function serveSitemap(req, res) {
|
||||
try {
|
||||
const xml = buildSitemapXml()
|
||||
// text/xml is what Google documents for sitemaps; charset for clarity
|
||||
const headers = {
|
||||
'Content-Type': 'application/xml; charset=utf-8',
|
||||
// Short cache so new pages appear soon after deploys
|
||||
'Content-Type': 'text/xml; charset=utf-8',
|
||||
'Cache-Control': 'public, max-age=300',
|
||||
'X-Robots-Tag': 'noindex', // the sitemap document itself need not be indexed as a page
|
||||
}
|
||||
if (req.method === 'HEAD') {
|
||||
res.writeHead(200, {
|
||||
...headers,
|
||||
'Content-Length': Buffer.byteLength(xml),
|
||||
'Content-Length': Buffer.byteLength(xml, 'utf8'),
|
||||
})
|
||||
res.end()
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user