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.
|
* 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://www.sitemaps.org/protocol.html
|
||||||
|
* @see https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap
|
||||||
*/
|
*/
|
||||||
function buildSitemapXml() {
|
function buildSitemapXml() {
|
||||||
const pages = walkHtmlFiles(DIST)
|
const pages = walkHtmlFiles(DIST)
|
||||||
@@ -268,64 +270,61 @@ function buildSitemapXml() {
|
|||||||
const excludeKey = urlPath.replace(/\/+$/, '') || '/'
|
const excludeKey = urlPath.replace(/\/+$/, '') || '/'
|
||||||
if (SITEMAP_EXCLUDE.has(excludeKey) || SITEMAP_EXCLUDE.has(urlPath)) continue
|
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)
|
const existing = byPath.get(urlPath)
|
||||||
if (existing && existing.lastmod >= lastmod) continue
|
if (existing && existing.lastmod >= lastmod) continue
|
||||||
|
|
||||||
|
// Absolute HTTPS loc only — required by the protocol
|
||||||
|
const loc = SITE_ORIGIN + (urlPath === '/' ? '/' : urlPath)
|
||||||
byPath.set(urlPath, {
|
byPath.set(urlPath, {
|
||||||
loc: SITE_ORIGIN + (urlPath === '/' ? '/' : urlPath),
|
loc,
|
||||||
lastmod,
|
lastmod,
|
||||||
changefreq: sitemapChangefreq(urlPath),
|
changefreq: sitemapChangefreq(urlPath),
|
||||||
priority: sitemapPriority(urlPath),
|
priority: sitemapPriority(urlPath),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stable sort: home first, then path
|
|
||||||
const entries = [...byPath.values()].sort((a, b) => {
|
const entries = [...byPath.values()].sort((a, b) => {
|
||||||
if (a.loc === `${SITE_ORIGIN}/`) return -1
|
if (a.loc === `${SITE_ORIGIN}/`) return -1
|
||||||
if (b.loc === `${SITE_ORIGIN}/`) return 1
|
if (b.loc === `${SITE_ORIGIN}/`) return 1
|
||||||
return a.loc.localeCompare(b.loc)
|
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"?>',
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||||
'<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">',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const e of entries) {
|
for (const e of entries) {
|
||||||
lines.push(' <url>')
|
parts.push(
|
||||||
lines.push(` <loc>${escapeXml(e.loc)}</loc>`)
|
' <url>',
|
||||||
lines.push(` <lastmod>${escapeXml(e.lastmod)}</lastmod>`)
|
` <loc>${escapeXml(e.loc)}</loc>`,
|
||||||
lines.push(` <changefreq>${escapeXml(e.changefreq)}</changefreq>`)
|
` <lastmod>${escapeXml(e.lastmod)}</lastmod>`,
|
||||||
lines.push(` <priority>${escapeXml(e.priority)}</priority>`)
|
` <changefreq>${escapeXml(e.changefreq)}</changefreq>`,
|
||||||
// Self-referencing hreflang (single-language site)
|
` <priority>${escapeXml(e.priority)}</priority>`,
|
||||||
lines.push(
|
' </url>'
|
||||||
` <xhtml:link rel="alternate" hreflang="en" href="${escapeXml(e.loc)}" />`
|
|
||||||
)
|
)
|
||||||
lines.push(
|
|
||||||
` <xhtml:link rel="alternate" hreflang="x-default" href="${escapeXml(e.loc)}" />`
|
|
||||||
)
|
|
||||||
lines.push(' </url>')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push('</urlset>')
|
parts.push('</urlset>')
|
||||||
lines.push('')
|
// Trailing newline for POSIX-friendly files
|
||||||
return lines.join('\n')
|
return parts.join('\n') + '\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
function serveSitemap(req, res) {
|
function serveSitemap(req, res) {
|
||||||
try {
|
try {
|
||||||
const xml = buildSitemapXml()
|
const xml = buildSitemapXml()
|
||||||
|
// text/xml is what Google documents for sitemaps; charset for clarity
|
||||||
const headers = {
|
const headers = {
|
||||||
'Content-Type': 'application/xml; charset=utf-8',
|
'Content-Type': 'text/xml; charset=utf-8',
|
||||||
// Short cache so new pages appear soon after deploys
|
|
||||||
'Cache-Control': 'public, max-age=300',
|
'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') {
|
if (req.method === 'HEAD') {
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
...headers,
|
...headers,
|
||||||
'Content-Length': Buffer.byteLength(xml),
|
'Content-Length': Buffer.byteLength(xml, 'utf8'),
|
||||||
})
|
})
|
||||||
res.end()
|
res.end()
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user