From aa78b43c34e8d08f472569493c81db0b7a63d886 Mon Sep 17 00:00:00 2001 From: Neeldhara Misra Date: Sat, 13 Jun 2026 22:01:17 +0200 Subject: [PATCH] Create minimal blog homepage --- .dockerignore | 4 ++ .forgejo/workflows/build.yml | 16 +++++ .gitignore | 3 + Dockerfile | 11 +++ README.md | 10 +++ deploy/nginx.conf | 10 +++ package.json | 9 +++ scripts/build.mjs | 129 +++++++++++++++++++++++++++++++++++ 8 files changed, 192 insertions(+) create mode 100644 .dockerignore create mode 100644 .forgejo/workflows/build.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 deploy/nginx.conf create mode 100644 package.json create mode 100644 scripts/build.mjs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..bd0f7e3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.git +dist +node_modules +.DS_Store diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml new file mode 100644 index 0000000..d4c4a23 --- /dev/null +++ b/.forgejo/workflows/build.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b186605 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +dist +node_modules +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..122c74e --- /dev/null +++ b/Dockerfile @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..a1bc834 --- /dev/null +++ b/README.md @@ -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 +``` diff --git a/deploy/nginx.conf b/deploy/nginx.conf new file mode 100644 index 0000000..0e44548 --- /dev/null +++ b/deploy/nginx.conf @@ -0,0 +1,10 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ =404; + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d63db35 --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "name": "neeldhara-blog-home", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "node scripts/build.mjs" + } +} diff --git a/scripts/build.mjs b/scripts/build.mjs new file mode 100644 index 0000000..89e893a --- /dev/null +++ b/scripts/build.mjs @@ -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(/</g, "<") + .replace(/&/g, "&"); +} + +function escapeHtml(value) { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +function textFrom(block, tag) { + const match = block.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)`, "i")); + return match ? decodeXml(match[1].trim()) : ""; +} + +function parseItems(xml) { + return [...xml.matchAll(/([\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 `
  • ${date}${escapeHtml(post.title)}
  • `; +} + +function renderBlog([slug, label], posts) { + return `
  • ${escapeHtml(label)} +
      + ${posts.map(renderPost).join("\n ")} +
    +
  • `; +} + +const entries = await Promise.all( + blogs.map(async (blog) => [blog, await getPosts(blog[0])]), +); + +const html = ` + + + + + neeldhara.blog + + + +
    +

    neeldhara.blog

    +
      + ${entries.map(([blog, posts]) => renderBlog(blog, posts)).join("\n ")} +
    +
    + + +`; + +await mkdir(outDir, { recursive: true }); +await writeFile(new URL("index.html", outDir), html);