import fs from "fs"; import process from "process"; import { Feed } from "feed"; import articlesConfig from "./articles/config.json"; function getFiles(dir: string): string[] { return fs.readdirSync(dir).flatMap((name) => { const path = `${dir}/${name}`; if (fs.statSync(path).isDirectory()) { return getFiles(path); } else { return [path]; } }); } const METADATA_BLOCK_REGEX = /^---\n([\s\S]*?)---\n/m; const METADATA_REGEX = /^(\w+):(.*)$/gm; function readArticleMetadata(path: string): Record | null { const content = fs.readFileSync(path, { encoding: "utf8" }); const match: RegExpExecArray | null = METADATA_BLOCK_REGEX.exec(content); if (!match?.[1]) { console.warn(`No metadata for: ${path}`); return null; } let subMatch: RegExpExecArray | null = null; const metadata: Record = { path: path.replaceAll("/index.md", ""), }; do { subMatch = METADATA_REGEX.exec(match[1]); if (subMatch?.[1] && subMatch[2]) { metadata[subMatch[1]] = subMatch[2].trim(); } } while (subMatch); return metadata; } function formatArticlePage( metadata: Record, baseHtml: string, ): string { let outHtml = baseHtml; outHtml = outHtml.replace(/<.*?property="og:url".*?>/gm, ""); outHtml = outHtml.replace( /<\/head>/gm, `\n`, ); const blog_title = articlesConfig.title.replace(/(<([^>]+)>)/gi, "").trim(); if (metadata.title) { const title = metadata.title.replace(/(<([^>]+)>)/gi, "").trim(); outHtml = outHtml.replace( /.*?<\/title>/gm, `<title>${blog_title} — ${title}`, ); outHtml = outHtml.replace(/<.*?property="og:title".*?>/gm, ""); outHtml = outHtml.replace( /<\/head>/gm, `\n`, ); outHtml = outHtml.replace(/<.*?property="og:description".*?>/gm, ""); outHtml = outHtml.replace( /<\/head>/gm, `\n`, ); } if (metadata.thumbnail) { outHtml = outHtml.replace(/<.*?property="og:image".*?>/gm, ""); outHtml = outHtml.replace( /<\/head>/gm, `\n`, ); } return outHtml; } function addFeedArticle(metadata: Record, feed: Feed) { if (metadata.draft !== "true") { feed.addItem({ title: metadata.title.replace(/(<([^>]+)>)/gi, "").trim(), id: metadata.path, link: `${articlesConfig.base_url}${metadata.path}/`, date: new Date(Date.parse(metadata.date)), image: metadata.thumbnail.replace( "./", articlesConfig.base_url + metadata.path + "/", ), }); } } const indexContent = fs.readFileSync("dist/index.html", { encoding: "utf8" }); if (!indexContent) { console.error("Could not read dist/index.html"); process.exit(1); } const metadatas = getFiles("articles") .filter((path) => /\/index.md$/.exec(path)) .map((path) => readArticleMetadata(path)) .filter((metadata) => !!metadata); const feed = new Feed({ title: articlesConfig.title.replace(/(<([^>]+)>)/gi, "").trim(), id: articlesConfig.base_url, link: articlesConfig.base_url, language: articlesConfig.lang, favicon: articlesConfig.base_url + "articles/favicon.ico", generator: "md-blog", feedLinks: { json: articlesConfig.base_url + "feed.json", atom: articlesConfig.base_url + "atom.xml", rss: articlesConfig.base_url + "rss", }, updated: new Date( Math.max( 0, ...metadatas .filter((metadata) => metadata.draft !== "true") .map((metadata) => Date.parse(metadata.date)), ), ), }); metadatas.forEach((metadata) => { fs.writeFileSync( `dist/${metadata.path}/index.html`, formatArticlePage(metadata, indexContent), ); console.info(`Wrote dist/${metadata.path}/index.html`); addFeedArticle(metadata, feed); }); fs.writeFileSync("dist/feed.json", feed.json1()); console.info(`Wrote dist/feed.json`); fs.writeFileSync("dist/atom.xml", feed.atom1()); console.info(`Wrote dist/atom.xml`); fs.writeFileSync("dist/rss.xml", feed.rss2()); console.info(`Wrote dist/rss.xml`);