Create minimal blog homepage
Some checks are pending
Build / build (push) Waiting to run

This commit is contained in:
Neeldhara Misra 2026-06-13 22:01:17 +02:00
commit aa78b43c34
8 changed files with 192 additions and 0 deletions

4
.dockerignore Normal file
View file

@ -0,0 +1,4 @@
.git
dist
node_modules
.DS_Store

View file

@ -0,0 +1,16 @@
name: Build
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: docker
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm run build

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
dist
node_modules
.DS_Store

11
Dockerfile Normal file
View file

@ -0,0 +1,11 @@
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json ./
COPY scripts ./scripts
RUN npm run build
FROM nginx:1.27-alpine AS runtime
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html

10
README.md Normal file
View file

@ -0,0 +1,10 @@
# neeldhara.blog
Minimal static homepage for `neeldhara.blog`.
The build script fetches the RSS feed from each blog and writes `dist/index.html`
with the latest two entries per blog.
```sh
npm run build
```

10
deploy/nginx.conf Normal file
View file

@ -0,0 +1,10 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}

9
package.json Normal file
View file

@ -0,0 +1,9 @@
{
"name": "neeldhara-blog-home",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "node scripts/build.mjs"
}
}

129
scripts/build.mjs Normal file
View file

@ -0,0 +1,129 @@
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);