A follow-up to my first post.
Sat, 4 Oct 2025
So I just learned I can directly commit .md files like I am using for the blog to my git, let Netlify convert them to html with the styling I want and auto generate an index/overview over all my blog posts.
That means I only ever see the .md files in my ./blog/ folder in git but deleting them there also deletes the post and updates the index. Very fancy.
This is the ./scripts/generate-blog-index.mjs:
import { readdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
import { join, basename } from "node:path";
import { marked } from "marked";
const BLOG_DIR = "blog";
const OUT = join(BLOG_DIR, "index.html");
const HEADER = `<!doctype html><html lang=en>
<meta name=viewport content="width=device-width,initial-scale=1">
<link rel=icon href=data:,>
<style>:root{color-scheme:light dark}body{margin:0 auto;line-height:1.5;max-width:65ch;padding:1rem}a{display:block}small a{color:gray;text-decoration:none}small a:hover{text-decoration:underline}pre{background:#eee;padding:.5em;overflow-x:auto;border-radius:.3em}@media(prefers-color-scheme:dark){pre{background:#222}}code{font-family:monospace}</style>
<small><a href="/blog/">Back to overview</a></small>
`;
for (const file of readdirSync(BLOG_DIR)) {
if (!file.endsWith(".md")) continue;
const mdPath = join(BLOG_DIR, file);
const htmlName = basename(file, ".md") + ".html";
const htmlPath = join(BLOG_DIR, htmlName);
const md = readFileSync(mdPath, "utf8");
const htmlBody = marked.parse(md);
writeFileSync(htmlPath, HEADER + htmlBody);
unlinkSync(mdPath); // remove .md after conversion
console.log(`Converted ${file} → ${htmlName} and removed original`);
}
const files = readdirSync(BLOG_DIR)
.filter(f => f.endsWith(".html") && f !== "index.html");
function decodeEntities(s) {
return s
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, '"')
.replace(/'/g, "'");
}
function extractH2H6(html) {
const h2 = html.match(/<h2\b[^>]*>([\s\S]*?)<\/h2>/i);
const h6 = html.match(/<h6\b[^>]*>([\s\S]*?)<\/h6>/i);
const title = h2 ? decodeEntities(h2[1].trim()) : "";
const date = h6 ? decodeEntities(h6[1].trim()) : "";
return { title, date };
}
function labelFromName(name) {
const m = name.match(/^(\d{6})/);
if (!m) return name;
const yy = parseInt(m[1].slice(0,2),10);
const mm = m[1].slice(2,4);
const dd = m[1].slice(4,6);
const yyyy = 2000 + yy;
return `${yyyy}-${mm}-${dd}`;
}
const posts = files.map(f => {
const html = readFileSync(join(BLOG_DIR, f), "utf8");
const { title, date } = extractH2H6(html);
const match = f.match(/^(\d{6})(?:_(\d+))?/);
const base = match ? match[1] : "000000";
const sub = match && match[2] ? parseInt(match[2], 10) : 0;
const slug = f.replace(/\.html$/i, "");
return { file: f, slug, title: title || base, date, base, sub };
}).sort((a, b) => {
if (b.base !== a.base) return b.base.localeCompare(a.base);
return b.sub - a.sub;
});
const doc = `<!doctype html><html lang=en>
<meta name=viewport content="width=device-width,initial-scale=1">
<title>Blog</title>
<link rel=icon href=data:,>
<style>:root{color-scheme:light dark}body{margin:0 auto;max-width:65ch;padding:1rem;line-height:1.5}ul{padding-left:1.25rem}small{color:gray}small a{color:gray;text-decoration:none}small a:hover{text-decoration:underline}</style>
<small><a href="/">Back to home</a></small>
<h1>Blog</h1>
<ul>
${posts.map(p => `<li><a href="/blog/${p.slug}">${p.title}</a>${p.date ? `<br><small>${p.date}</small>` : ""}`).join("\n")}
</ul>`;
writeFileSync(OUT, doc);
console.log(`Wrote ${OUT} with ${posts.length} posts`);