Skip to content
IrdiZPublic

About

Source of my blog, irdizeneli.dev. Astro.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

spinlock

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.

structure

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

dev

npm install
npm run dev          # http://localhost:4321
npx astro check      # type check
npm run build

writing a new post

  1. Add a markdown file at src/content/posts/<slug>.md. The slug must be lowercase letters, digits and dashes.
  2. Frontmatter (subtitle, tags, ogImage and draft are 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
    ---
  3. Optional: add a per-post OG card. Copy scripts/og-default.html to scripts/og-mypost.html, edit it, add render og-mypost to scripts/gen-og.sh, then run npm run og.

updating the debt tracker

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.

paintings

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.

Open Graph images

npm run og

Renders 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.

views and likes

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.

share on linkedin

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.

deploy

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.

license

MIT. Copyright (c) 2025-2026 Irdi Zeneli. The paintings are public domain.

About

Source of my blog, irdizeneli.dev. Astro.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages