I’ve been thinking about how i don’t want to write html anymore.
I got Claude to write me a Node script that takes a markdown file and generates an HTML file that matches the existing style of pages on here. Here’s the source:
#!/usr/bin/env node
// Convert markdown posts in thoughts/ to HTML.
//
// Write a post as thoughts/YYYY-MM-DD.md with frontmatter:
//
// ---
// title: my post title
// date: 2026-07-20
// ---
//
// Post body in markdown...
//
// Then run `node build_thoughts.js`. Every .md file in thoughts/ is
// converted to a .html file next to it (existing hand-written .html
// posts without a .md source are never touched), and new posts are
// linked from the Thoughts list in index.html, sorted newest-first.
// Requires pandoc.
const fs = require("fs");
const path = require("path");
const { execFileSync } = require("child_process");
const THOUGHTS_DIR = path.join(__dirname, "thoughts");
const INDEX = path.join(__dirname, "index.html");
const MONTHS = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December",
];
function ordinal(n) {
if (n % 100 >= 11 && n % 100 <= 13) return `${n}th`;
return n + ({ 1: "st", 2: "nd", 3: "rd" }[n % 10] || "th");
}
// "2026-07-20" -> "20th July 2026"
function formatDate(isoDate) {
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(isoDate);
if (!m) throw new Error(`invalid date: ${isoDate}`);
return `${ordinal(Number(m[3]))} ${MONTHS[Number(m[2]) - 1]} ${m[1]}`;
}
function escapeHtml(s) {
return s
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
}
function parseFrontmatter(text, filename) {
const meta = {};
let body = text;
const match = /^---\s*\n([\s\S]*?)\n---\s*\n/.exec(text);
if (match) {
body = text.slice(match[0].length);
for (const line of match[1].split("\n")) {
const i = line.indexOf(":");
if (i !== -1) meta[line.slice(0, i).trim()] = line.slice(i + 1).trim();
}
}
const stem = path.basename(filename, ".md");
return {
title: meta.title || stem,
date: meta.date || stem, // filename is YYYY-MM-DD
body,
};
}
function template(title, date, body) {
return `<!DOCTYPE html>
<html>
<head>
<title>${title} | rjwebb.github.io</title>
<link rel="stylesheet" type="text/css" href="../style/style.css" />
</head>
<body>
<a href="../index.html">Back</a>
<h1>${title}</h1>
<em>written ${date}</em>
${body}
</body>
</html>
`;
}
function convert(mdPath) {
const { title, date, body } = parseFrontmatter(
fs.readFileSync(mdPath, "utf8"),
mdPath
);
const bodyHtml = execFileSync("pandoc", ["-f", "markdown", "-t", "html"], {
input: body,
encoding: "utf8",
})
.trimEnd()
.split("\n")
.map((line) => (line ? " " + line : ""))
.join("\n");
const outPath = mdPath.replace(/\.md$/, ".html");
fs.writeFileSync(
outPath,
template(escapeHtml(title), formatDate(date), bodyHtml)
);
return { outPath, title };
}
// Insert a link into the Thoughts <ul> in index.html, keeping the list
// sorted newest-first. Posts are named YYYY-MM-DD so names sort as dates.
function addToIndex(indexHtml, name, title) {
const li = [
" <li>",
` <a href="thoughts/${name}">${escapeHtml(title)}</a>`,
" </li>",
"",
].join("\n");
const linkRe = /<li>(?:(?!<\/li>)[\s\S])*?href="thoughts\/([^"]+)"[\s\S]*?<\/li>\s*\n/g;
let lastEnd = -1;
let match;
while ((match = linkRe.exec(indexHtml)) !== null) {
if (match[1] < name) {
// first existing post older than this one: insert before it,
// at the start of its line so indentation stays intact
const at = indexHtml.lastIndexOf("\n", match.index) + 1;
return indexHtml.slice(0, at) + li + indexHtml.slice(at);
}
lastEnd = match.index + match[0].length;
}
if (lastEnd !== -1) {
// older than every existing post: append after the last one
return indexHtml.slice(0, lastEnd) + li + indexHtml.slice(lastEnd);
}
return null;
}
function main() {
const mdFiles = fs
.readdirSync(THOUGHTS_DIR)
.filter((f) => f.endsWith(".md"))
.sort()
.map((f) => path.join(THOUGHTS_DIR, f));
if (mdFiles.length === 0) {
console.log("No .md files found in thoughts/");
return;
}
let indexHtml = fs.existsSync(INDEX) ? fs.readFileSync(INDEX, "utf8") : "";
let indexChanged = false;
for (const mdPath of mdFiles) {
const { outPath, title } = convert(mdPath);
const name = path.basename(outPath);
console.log(`${path.basename(mdPath)} -> ${name}`);
if (!indexHtml.includes(`thoughts/${name}`)) {
const updated = addToIndex(indexHtml, name, title);
if (updated) {
indexHtml = updated;
indexChanged = true;
console.log(` added to index.html: ${title}`);
} else {
console.log(" couldn't find the Thoughts list — add manually:");
console.log(
` <li><a href="thoughts/${name}">${escapeHtml(title)}</a></li>`
);
}
}
}
if (indexChanged) fs.writeFileSync(INDEX, indexHtml);
}
main();
I think this is so cool!!!