home/scripts/build.mjs
Neeldhara Misra aa78b43c34
Some checks are pending
Build / build (push) Waiting to run
Create minimal blog homepage
2026-06-13 22:01:17 +02:00

129 lines
3.2 KiB
JavaScript

import { mkdir, writeFile } from "node:fs/promises";
const blogs = [
["research", "research"],
["vibes", "vibes"],
["puzzles", "puzzles"],
["reflections", "reflections"],
["poetry", "poetry"],
["reviews", "reviews"],
["art", "art"],
];
const rssHostSuffix = process.env.RSS_HOST_SUFFIX ?? "neeldhara.email";
const outDir = new URL("../dist/", import.meta.url);
function decodeXml(value) {
return value
.replace(/'/g, "'")
.replace(/"/g, '"')
.replace(/>/g, ">")
.replace(/&lt;/g, "<")
.replace(/&amp;/g, "&");
}
function escapeHtml(value) {
return value
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function textFrom(block, tag) {
const match = block.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, "i"));
return match ? decodeXml(match[1].trim()) : "";
}
function parseItems(xml) {
return [...xml.matchAll(/<item>([\s\S]*?)<\/item>/gi)].slice(0, 2).map((match) => {
const block = match[1];
const pubDate = new Date(textFrom(block, "pubDate"));
return {
title: textFrom(block, "title"),
link: textFrom(block, "link"),
date: Number.isNaN(pubDate.valueOf()) ? "" : pubDate.toISOString().slice(0, 10),
};
});
}
async function getPosts(slug) {
const url = `https://${slug}.${rssHostSuffix}/rss.xml`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`${url} returned ${response.status}`);
}
return parseItems(await response.text());
}
function renderPost(post) {
const date = post.date ? `${escapeHtml(post.date)} ` : "";
return `<li>${date}<a href="${escapeHtml(post.link)}">${escapeHtml(post.title)}</a></li>`;
}
function renderBlog([slug, label], posts) {
return `<li>${escapeHtml(label)}
<ul>
${posts.map(renderPost).join("\n ")}
</ul>
</li>`;
}
const entries = await Promise.all(
blogs.map(async (blog) => [blog, await getPosts(blog[0])]),
);
const html = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>neeldhara.blog</title>
<style>
html { color-scheme: light; }
body {
margin: 0;
background: #fff;
color: #111;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
font-size: 16px;
line-height: 1.55;
}
main {
width: min(640px, calc(100% - 40px));
margin: 86px auto;
}
h1 {
margin: 0 0 18px;
font-size: 28px;
line-height: 1.2;
}
ul {
margin-top: 0;
margin-bottom: 0.75rem;
padding-left: 1.45rem;
}
li { margin: 0.18rem 0; }
a { color: #00e; }
a:visited { color: #551a8b; }
@media (max-width: 640px) {
main { margin: 42px auto; }
}
</style>
</head>
<body>
<main>
<h1>neeldhara.blog</h1>
<ul>
${entries.map(([blog, posts]) => renderBlog(blog, posts)).join("\n ")}
</ul>
</main>
</body>
</html>
`;
await mkdir(outDir, { recursive: true });
await writeFile(new URL("index.html", outDir), html);