diff --git a/eslint.config.mjs b/eslint.config.mjs index 0552609c2126..7fc04bdf812c 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -8,6 +8,7 @@ export default defineConfig([ globalIgnores([ "**/dist/", "**/examples/", + "**/printable.mdx", "src/content/loaders/_*.mdx", "src/content/plugins/_*.mdx", "src/content/contribute/Governance-*.mdx", diff --git a/package.json b/package.json index 2472d23c3e13..f15aaab9cb58 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "fetch-all": "run-s fetch-repos fetch", "prebuild": "npm run clean", "build": "run-s fetch-repos fetch:governance fetch content && webpack --config webpack.prod.mjs --config-node-env production && run-s printable content && webpack --config webpack.ssg.mjs --config-node-env production --env ssg", - "postbuild": "npm run sitemap", + "postbuild": "npm run sitemap && npm run rss", "build-test": "npm run build && http-server --port 4200 dist/", "serve-dist": "http-server --port 4200 dist/", "test": "npm run lint", @@ -47,6 +47,7 @@ "lint:prose": "vale --config='.vale.ini' src/content", "lint:links": "hyperlink -c 8 --root dist -r dist/index.html --canonicalroot https://webpack.js.org/ --internal --skip /plugins/extract-text-webpack-plugin/ --skip /printable --skip /contribute/Governance --skip https:// --skip http:// --skip sw.js --skip /vendor > internal-links.tap; cat internal-links.tap | tap-spot", "sitemap": "cd dist && sitemap-static --ignore-file=../sitemap-ignore.json --pretty --prefix=https://webpack.js.org/ > sitemap.xml", + "rss": "node src/scripts/generate-rss.mjs", "serve": "npm run build && sirv start ./dist --port 4000", "preprintable": "npm run clean-printable", "printable": "node ./src/scripts/concatenate-docs.mjs", diff --git a/src/components/Site/Site.jsx b/src/components/Site/Site.jsx index 523a9d4742c7..2736a344aad6 100644 --- a/src/components/Site/Site.jsx +++ b/src/components/Site/Site.jsx @@ -199,6 +199,12 @@ function Site(props) { location.pathname, )}`} /> + diff --git a/src/scripts/generate-rss.mjs b/src/scripts/generate-rss.mjs new file mode 100644 index 000000000000..f3cbe5501248 --- /dev/null +++ b/src/scripts/generate-rss.mjs @@ -0,0 +1,107 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.resolve(__dirname, "../.."); +const BASE_URL = "https://webpack.js.org"; + +function escapeXml(text) { + return String(text) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +function extractPubDate(node) { + const name = node.name || ""; + + const filenameDateMatch = name.match(/^(\d{4}-\d{2}-\d{2})-/); + if (filenameDateMatch) { + const date = new Date(filenameDateMatch[1]); + if (!Number.isNaN(date.getTime())) return date; + } + + const title = node.title || ""; + const titleDateMatch = title.match(/\((\d{4}-\d{2}-\d{2})\)/); + if (titleDateMatch) { + const date = new Date(titleDateMatch[1]); + if (!Number.isNaN(date.getTime())) return date; + } + + const filePath = path.resolve(ROOT, node.path); + try { + const stat = fs.statSync(filePath); + return stat.mtime; + } catch { + return new Date(); + } +} + +function formatRfc2822(date) { + return date.toUTCString(); +} + +function main() { + const contentPath = path.resolve(ROOT, "src/_content.json"); + const contentTree = JSON.parse(fs.readFileSync(contentPath, "utf8")); + + const blogSection = contentTree.children?.find( + (child) => child.name === "blog" && child.type === "directory", + ); + + if (!blogSection?.children) { + throw new Error("generate-rss: blog section not found in content tree"); + } + + const posts = blogSection.children + .filter( + (child) => + child.type === "file" && + (child.extension === ".mdx" || child.extension === ".md") && + child.name !== "index" && + child.name !== "printable" && + child.url !== "/blog/", + ) + .map((node) => ({ + title: node.title || "Untitled", + link: `${BASE_URL}${node.url}`, + pubDate: extractPubDate(node), + description: node.description || node.title || "Untitled", + })) + .toSorted((a, b) => b.pubDate.getTime() - a.pubDate.getTime()); + + const lastBuildDate = formatRfc2822(new Date()); + + const xml = ` + + + webpack Blog + ${BASE_URL}/blog/ + Announcements and updates from the webpack team + en + ${escapeXml(lastBuildDate)} + +${posts + .map( + (post) => ` + ${escapeXml(post.title)} + ${escapeXml(post.link)} + ${escapeXml(post.description)} + ${escapeXml(formatRfc2822(post.pubDate))} + ${escapeXml(post.link)} + `, + ) + .join("\n")} + + +`; + + const distPath = path.resolve(ROOT, "dist/feed.xml"); + fs.writeFileSync(distPath, xml, "utf8"); + console.log(`Successfully generated RSS feed at ${distPath}`); +} + +main();