Reviewed-on: #17
Personal Blog
A small static personal blog. No build step, no backend — plain HTML, CSS, and vanilla JavaScript.
Pages
index.html— home pagereading.html— reading list page (curated links grouped by category, rendered fromdata/reading-list.js)contact.html— contact page (opens the visitor's mail client with the form fields pre-filled via amailto:link)newsletter.html— newsletter signup page (posts the visitor's email to a configured serverless endpoint; no self-hosted backend or subscriber list management)
Newsletter signup
The signup form on newsletter.html POSTs the visitor's email to a serverless
endpoint. The client ships only the endpoint URL — no credential is ever sent
in the request:
NEWSLETTER_ENDPOINT— the serverless endpoint the form POSTs to. This is the only thing configured injs/newsletter-config.js, and it is enforcedhttps://-only at config load time and by tests (mirroring the protocol allowlist injs/reading-list.js).
Authentication is server-side only. The serverless function reads its API
token from platform env/secrets at deploy time — never from this repository and
never from any client-served asset. Do not add a token to js/newsletter-config.js
or anywhere else in the tree; anything committed here is public.
When the endpoint is unavailable or rejects the submission, the visitor sees a fixed, user-safe error message — the server-held token, the endpoint, the status code, and any raw response body are never surfaced. After a successful signup a confirmation message is shown.
Server-side validation (required follow-up on the function, not this diff):
the serverless function must authoritatively validate submissions before
processing — email syntax, length caps, pinned Content-Type, and rejection of
unknown fields. Client-side checks here are UX only and are bypassable by
direct API calls.
Residual signup-abuse risk (accepted, out of scope): this pass adds no spam filtering or captcha. Scripted signups remain possible, so the serverless function must mitigate abuse server-side with rate limiting and an origin allowlist.
Reading list
The reading list is a single data file: data/reading-list.js. The page code
renders whatever is in it, so it can grow to hundreds of entries without code
changes.
To add a link, edit data/reading-list.js only — add an entry with a title,
a url, a category, and an optional one-line note:
{
category: "Engineering",
title: "A new article",
url: "https://example.com/article",
note: "Optional one-liner shown under the title.",
},
Categories appear on the page in the order they are first used; entries keep the order they are listed in.
Development
Serve the directory with any static file server, e.g.:
python3 -m http.server 8000
Then open http://localhost:8000.
Tests
Tests use Node's built-in test runner (no dependencies to install):
npm test
Project layout
index.html Home page
reading.html Reading list page
contact.html Contact page
newsletter.html Newsletter signup page
css/style.css Global + responsive styles
data/reading-list.js Curated reading list data (edit to add links)
js/newsletter-config.js Newsletter endpoint (non-secret, https-only) — edit at deploy time
js/newsletter.js Newsletter signup wiring: credential-free POST + user-safe errors (tested)
js/mailto.js Pure mailto: URL builder (unit tested)
js/contact.js Contact form wiring (browser + tests)
js/reading-list.js Reading list renderer: data file -> grouped HTML (tested)
tests/ Node built-in test suite
.gitea/workflows/ ci.yml runs `npm test` plus a gitleaks secret scan (fails on any hit) on PRs and pushes to main