import { join } from "node:path"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import { gzipSync } from "node:zlib"; import { fdir } from "fdir"; import { BASE_URL, BUILD_OUT_ROOT } from "../libs/env/index.js"; const SITEMAP_BASE_URL = BASE_URL.replace(/\/$/, ""); interface SitemapEntry { slug: string; modified?: string; } export async function buildSitemap( entries: SitemapEntry[], { slugPrefix = "", pathSuffix = [], }: { slugPrefix?: string; pathSuffix?: string[] } ) { const txt = entries.map(({ slug }) => `${slugPrefix}${slug}`).join("\n"); const xml = makeSitemapXML(slugPrefix, entries); const dirPath = join( BUILD_OUT_ROOT, "sitemaps", ...pathSuffix.map((p) => p.toLowerCase()) ); await mkdir(dirPath, { recursive: true }); const txtPath = join(dirPath, "sitemap.txt"); const xmlPath = join(dirPath, "sitemap.xml.gz"); await Promise.all([ writeFile(txtPath, txt, "utf-8"), writeFile(xmlPath, gzipSync(xml)), ]); return xmlPath; } export async function buildSitemapIndex() { return Promise.all([buildSitemapIndexTXT(), buildSitemapIndexXML()]); } export async function buildSitemapIndexTXT() { const sitemaps = new fdir() .filter((p) => p.endsWith("/sitemap.txt")) .withFullPaths() .crawl(join(BUILD_OUT_ROOT, "sitemaps")) .sync(); const file = join(BUILD_OUT_ROOT, "sitemap.txt"); const content = await makeSitemapIndexTXT(sitemaps); await writeFile(file, content, "utf-8"); return sitemaps.sort().map((fp) => fp.replace(BUILD_OUT_ROOT, "")); } export async function buildSitemapIndexXML() { const sitemaps = new fdir() .filter((p) => p.endsWith("/sitemap.xml.gz")) .withFullPaths() .crawl(join(BUILD_OUT_ROOT, "sitemaps")) .sync() .sort() .map((fp) => fp.replace(BUILD_OUT_ROOT, "")); const file = join(BUILD_OUT_ROOT, "sitemap.xml"); await writeFile(file, makeSitemapIndexXML(sitemaps)); return sitemaps.sort().map((fp) => fp.replace(BUILD_OUT_ROOT, "")); } function makeSitemapXML(prefix: string, docs: SitemapEntry[]) { const sortedDocs = docs.slice().sort((a, b) => a.slug.localeCompare(b.slug)); // Based on https://support.google.com/webmasters/answer/183668?hl=en return [ '', '', ...sortedDocs.map((doc) => { const loc = `${SITEMAP_BASE_URL}${prefix}${doc.slug}`; const modified = doc.modified ? `${doc.modified.toString().split("T")[0]}` : ""; return `${loc}${modified}`; }), "", "", ].join("\n"); } export function makeSitemapIndexXML(paths: string[]) { const sortedPaths = paths.slice().sort(); // Based on https://support.google.com/webmasters/answer/75712 return [ '', '', ...sortedPaths.map((path) => { return ( "" + `${SITEMAP_BASE_URL}${path}` + `${new Date().toISOString().split("T")[0]}` + "" ); }), "", ].join("\n"); } /** * Creates a global text sitemap by merging all text sitemaps. */ export async function makeSitemapIndexTXT(paths: string[]) { const maps = await Promise.all(paths.map((p) => readFile(p, "utf-8"))); const urls = maps.join("\n").split("\n").filter(Boolean); return urls.sort().join("\n"); }