Personal site. Gleam SSG that fetches from the AT Protocol (Bluesky PDS + Tangled) at build time, hydrates client-side with a Lustre component tree. Album covers, artist photos, and the profile avatar/banner are mirrored into the site at build time, so the visitor's browser never has to fetch from Cover Art Archive, Wikimedia, or the PDS directly — every image is served from /img/... on the site itself.
nix develop # or: direnv reload
just refresh # data pipeline: plays + covers + mirrored images
just build # codegen → client JS → static site → ./dist/
just test # run all tests
just clean # wipe build artifacts
just new my-slug # scaffold a new postNeeds Gleam 1.17+ and Erlang/OTP 28+ — nix develop provides everything.
| Variable | Default | Description |
|---|---|---|
BLOG_URL |
https://karitham.dev |
Base URL for OG tags, RSS, and links |
BLOG_URL="http://localhost:8000" just buildThe Music section's stats, covers, and page links come from a data pipeline that runs before just build:
just refreshrefresh downloads your play history as a CAR file from the PDS, aggregates top-N artists/albums/tracks per time range, resolves album covers (Cover Art Archive), artist photos (MusicBrainz → Wikidata → Wikimedia Commons), and MusicBrainz page links, then writes priv/cache/plays-stats.json and mirrors every image into priv/cache/img/ for the SSG to copy into dist/img/.
Every lookup is cache-first with per-endpoint rate limiting, jittered retries, and Retry-After handling; with a warm cache refresh is fully offline and only new plays touch the network. A failed or interrupted run never corrupts the caches (atomic writes), and the next run simply re-does what didn't finish.
CI runs just refresh && just build on every push and caches priv/cache/cover-cache.json + priv/cache/img between runs, so deploys only resolve and download what's new. The whole priv/cache dir is gitignored and wiped by just clean.
just new my-post-slugScaffolds priv/posts/my-post-slug/index.md with today's date and draft: true.
Flip draft: false to publish.
Or create a directory manually:
priv/posts/my-post/
index.md
hero.png # optional hero/banner image
diagram.png # → /posts/my-post/diagram.png
Frontmatter:
---
title: My Post
description: short summary
date: 2026-07-18
tags: [gleam, atproto]
draft: true
image: hero.png
---Slug is the directory name. Build validates required fields and fails with a clear error if something's wrong.
Four components:
| Component | Role |
|---|---|
shared/ |
Model types, generated decoders, view components (compiled to both Erlang and JS) |
| root | SSG — fetches data, mirrors images, renders, writes dist/ |
client/ |
Browser bundle — fetches fresh data on page load (profile images rewritten to the local mirrors), polls plays every 30s |
tools/parse-plays |
Rust CLI — CAR → play stats, cover/artist resolution, image mirroring |
just test
# or:
gleam test && cd shared && gleam test