Systems nerd vs. student debt. Shipping indie products until one wins. Building in public.
The personal blog of Irdi Zeneli, live at irdizeneli.dev. On X as @spinlock_01.
The design is a 1991 photocopied Berlin rave flyer: true black, grain, stacked uppercase type, and one painting per page, by Claude Monet or Gustav Klimt. The terminal block and the debt bar copy the X banner. Posts are typed by hand, mostly. The site says "strict no-AI policy on the writing. (just kidding.)" and means it.
Built with Astro 5 and one stylesheet. Hosted on Vercel.
src/
site.ts # site name, bio, links, debt numbers (the only place for these)
lib/paintings.ts # the painting registry (artist, url, size, credit, alt, crop)
lib/posts.ts # published posts, newest first (drafts show in dev only)
lib/stats.ts # Upstash Redis counters for views and likes
content/posts/ # markdown posts (one .md per post)
content.config.ts # frontmatter schema
components/ # Nav, Footer, ArtPanel, Wordmark, Terminal, DebtTracker, FollowBar
layouts/Layout.astro # base layout, fonts, OG / Twitter meta, Vercel Analytics
pages/index.astro # home: painting split, terminal, projects, posts
pages/posts/[...slug] # post page: sticky painting, views, likes, share on linkedin
pages/404.astro
pages/rss.xml.ts # hand-built RSS 2.0 feed
pages/api/ # views, likes, stats, linkedin share redirect
styles/global.css # all styling lives here
scripts/
og-default.html # OG card template (1200x630)
gen-og.sh # renders OG cards to public/og-*.png with headless Chrome
public/
og-default.png, favicon.svg, me.jpg
npm install
npm run dev # http://localhost:4321
npx astro check # type check
npm run build- Add a markdown file at
src/content/posts/<slug>.md. The slug must be lowercase letters, digits and dashes. - Frontmatter (
subtitle,tags,ogImageanddraftare optional):--- title: my new post subtitle: a short one-line hook date: 2026-06-01 excerpt: shows on the home page and as og:description tags: [whatever, you, want] ogImage: /og-mypost.png # falls back to /og-spinlock.jpg painting: wheatstacks # key from src/lib/paintings.ts, falls back to the parliament draft: true # shows in npm run dev only, never in a build ---
- Optional: add a per-post OG card. Copy
scripts/og-default.htmltoscripts/og-mypost.html, edit it, addrender og-myposttoscripts/gen-og.sh, then runnpm run og.
Edit DEBT_PAID in src/site.ts when a payment lands. Also edit PAID in the X banner
script (make_banner.py) so the site and the banner show the same number.
All paintings are by Claude Monet (died 1926) or Gustav Klimt (died 1918) and are in the public domain. The files come from
Wikimedia Commons and are self-hosted in public/art/ as WebP at 640, 1280 and 1920 px wide.
src/lib/paintings.ts holds each painting's file name, size, credit line, alt text and crop
point. To change a painting, add the three files and edit only that registry.
| page | painting | museum |
|---|---|---|
| home (rotation) | Monet, San Giorgio Maggiore at Dusk, 1908 | Fondation Beyeler |
| home (rotation) | Klimt, Birch Forest, 1903 | Private collection |
| home (rotation) | Klimt, Farm Garden with Sunflowers, 1907 | Belvedere, Vienna |
| home (rotation) | Klimt, Island in the Attersee, 1901-02 | Private collection |
| home (rotation) | Klimt, Pear Tree, 1903 | Busch-Reisinger Museum, Harvard |
| post | The Houses of Parliament, Sunset, 1903 | National Gallery of Art, Washington |
| 404 | The Magpie, 1868-69 | Musée d'Orsay, Paris |
| post: two clip paths | Stacks of Wheat (End of Summer), 1890-91 | Art Institute of Chicago |
The home page picks one painting from its rotation per visitor and keeps it for a day
(home_art in localStorage; a new browser or an incognito window gets a new pick). The pick
happens in an inline script before the image loads, so there is no double download, and visitors
without JavaScript see San Giorgio. The pool lives in src/pages/index.astro.
A post can pick its own painting with painting: <key> in its frontmatter. Add the painting to
the registry first (three WebP files plus one entry), then use its key.
npm run ogRenders every listed scripts/og-*.html template into public/<name>.png at 1200x630 with
headless Chrome. It finds Chrome on macOS, Linux (google-chrome, chromium) and Windows
(Git Bash). No npm dependencies.
Each post counts unique views and likes in Upstash Redis (free tier). Copy .env.example to
.env.local for local dev and fill in UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN.
On Vercel, add the same variables in Project Settings. Without them the counters show 0.
A per-browser id in localStorage (vw_uid) dedupes views and likes. Do not rename the key.
The share button on each post goes to /api/linkedin/start, which redirects to LinkedIn's
standard share dialog with the post URL. The reader writes the caption in LinkedIn. No OAuth,
no API keys.
Hybrid build: pages are prerendered, /api/* routes run as Vercel serverless functions.
Connect the GitHub repo in Vercel, or run vercel with the Vercel CLI.
MIT. Copyright (c) 2025-2026 Irdi Zeneli. The paintings are public domain.