This commit is contained in:
commit
aa78b43c34
8 changed files with 192 additions and 0 deletions
4
.dockerignore
Normal file
4
.dockerignore
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
.git
|
||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
16
.forgejo/workflows/build.yml
Normal file
16
.forgejo/workflows/build.yml
Normal 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
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
11
Dockerfile
Normal file
11
Dockerfile
Normal 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
10
README.md
Normal 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
10
deploy/nginx.conf
Normal 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
9
package.json
Normal 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
129
scripts/build.mjs
Normal 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(/</g, "<")
|
||||||
|
.replace(/&/g, "&");
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return value
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
Loading…
Add table
Add a link
Reference in a new issue