The personal website of Khaled Waleed, a design engineer in Baghdad, Iraq. A quiet, atmospheric site built with SvelteKit and Go-flavoured sensibilities, where every page is its own dimly-lit room hung with a public-domain Northern Romantic painting.
🔗 Live: khaledwaleed.com
It's a portfolio, a blog, and a small personal museum. Each route is a room, and each room is paired with a famous public-domain painting (Friedrich, Dahl, Rembrandt, Vermeer, Wright of Derby…) that sets its colour palette and mood. The result is a calm, reading-first site rather than a dashboard of widgets.
Almost everything is prerendered to static HTML at build time. The single exception is /music, which runs live on the edge to read recent listening data from Spotify. Both /music and /library are hidden for now; the worker answers them with a 302 to /likes.
| Concern | Choice |
|---|---|
| Framework | SvelteKit 2 with Svelte 5 runes |
| Styling | Tailwind CSS 4 + @tailwindcss/typography |
| Content | mdsvex (.svx) for writing |
| Language | TypeScript |
| OG images | satori + resvg (generated at build) |
| Fonts | Self-hosted Fraunces + Libre Franklin via Fontsource |
| Runtime / PM | Node 24 + pnpm 11 |
| Hosting | Cloudflare Pages (@sveltejs/adapter-cloudflare) |
pnpm install
pnpm dev # start the dev server
pnpm dev --open # …and open it in the browserpnpm build # production build (static + edge functions)
pnpm preview # preview the production build locally
pnpm check # type-check with svelte-check
pnpm test # unit tests (vitest)
pnpm lint # eslint (antfu config, formatting included)
pnpm format # eslint --fixCI (GitHub Actions) runs lint → check → test → build on every push and PR to main.
Copy .env.example to .env and fill in what you need. Everything renders without them, but /music shows an empty state, and /manage can't save TMDB snapshots until TMDB_API_KEY is set. The public /films page reads its data (including the TMDB snapshot) straight from D1 and needs no keys.
| Variable | Used for |
|---|---|
TMDB_API_KEY |
/manage saves & the /api/tmdb/* endpoints (search + metadata snapshots) |
SPOTIFY_CLIENT_ID |
Live /music page: top tracks, artists, recently played |
SPOTIFY_CLIENT_SECRET |
Live /music page |
SPOTIFY_REFRESH_TOKEN |
Live /music page (obtain via node scripts/spotify-auth.ts) |
In production, TMDB_API_KEY and the three SPOTIFY_* values are set as Cloudflare Pages environment variables.
Analytics are optional and off by default: paste a Cloudflare Web Analytics beacon token into cloudflareAnalyticsToken in src/lib/site.ts to enable the cookieless beacon in production builds.
src/
├─ app.css # design tokens, per-room palettes, typography
├─ app.html # document shell
├─ lib/
│ ├─ site.ts # ← central config: bio, socials, paintings, room map
│ ├─ posts.ts # typed index of writing posts (auto-discovered)
│ ├─ films.ts # Film record types (ratings live in D1)
│ ├─ tmdb.ts # shared TMDB types (search results, film metadata)
│ ├─ server/og.ts # shared satori → PNG renderer for Open Graph cards
│ ├─ index.ts # barrel exports
│ └─ components/ # Seo, JsonLd, SchemaOrg, Container, CommandPalette, …
├─ posts/ # writing, as .svx (mdsvex)
└─ routes/
├─ +layout.svelte # shell: nav, footer, room/painting, command palette
├─ +page.svelte # home
├─ about · projects · likes · library · films · music · contact · writing
├─ og.png/ # site-wide OG card · per-essay cards at writing/[slug]/og.png
└─ sitemap.xml · robots.txt · rss.xml (styled by static/rss.xsl)
scripts/ # data tooling (see below)
static/ # paintings, logos, manifest, etc.
src/lib/site.ts is the heart of the site: bio, contact details, social links, and the painting-per-room map all live there. Edit values in one place rather than hunting through components.
| Route | What it is |
|---|---|
/ |
Home / hero, the canonical profile page |
/story |
Longer bio, career history, CV download (/about 301s here) |
/projects |
The catalogue of shipped work: products, client work, government platforms, open source |
/writing |
Essays (Markdown via mdsvex), with /writing/[slug] and per-essay OG cards |
/library |
Books (hidden for now, 302s to /likes) |
/films |
A ledger of ~225 rated films & shows, server-rendered from D1, searchable, with stats |
/music |
Live top tracks & artists from Spotify (hidden for now, 302s to /likes) |
/likes |
A catalogue of obsessions |
/tools |
The hardware, software, and services in daily use (/uses 301s here) |
/contact |
Ways to get in touch |
The scripts/ folder holds small Node scripts for maintaining content:
| Script | Purpose |
|---|---|
fetch-paintings.ts |
Download & convert room paintings to AVIF/WebP (via sharp) |
fetch-paintings-direct.ts |
Same, for paintings that need a specific Commons file |
generate-painting-sizes.ts |
Emit 720/1280px responsive variants + the painting-sizes.json manifest |
generate-icons.ts |
Render the apple-touch and manifest PNG icons from the wordmark |
Run any of them with node scripts/<name>.ts (Node 24 runs TypeScript directly).
- Writing: drop a
.svxfile undersrc/posts/; it is auto-discovered (list, RSS, sitemap, palette, OG card, prev/next all follow). - Films: add & rate titles from
/manage(a noindex admin page behind Cloudflare Access, backed by/api/manage): search TMDB, pick a title, set rating / watch dates / notes. A denormalized TMDB snapshot (title, year, directors, poster, runtime, genres) is written to D1 at save time and the poster is copied into R2, so the public page needs no TMDB at request time. - A new room: add a painting to the
paintingsmap insite.ts, map it inroomForPath, add a[data-room]palette inapp.css, drop the image instatic/paintings/, and rungenerate-painting-sizes.ts.
The site builds with @sveltejs/adapter-cloudflare and runs as the khaledwaleed-com Worker (Workers Builds deploys every push to main). Prerendered pages come from static assets; /films, /music, /manage and the APIs render in the Worker.
The film log lives in the D1 database khaledwaleed (schema in migrations/, applied with vpx wrangler d1 migrations apply khaledwaleed --remote) and its posters in the R2 bucket khaledwaleed-posters, served from posters.khaledwaleed.com. For local dev, run the same command with --local and seed it with a wrangler d1 export of the remote database. legacy-api/ is a separate Worker on api.khaledwaleed.com, the old PocketBase host: it 301s old poster URLs to R2 and answers 410 to everything else (vpx wrangler deploy -c legacy-api/wrangler.jsonc).
- Paintings are in the public domain; each is credited (artist, year, museum) in
src/lib/site.tsand on the page footers. - Film metadata & posters from The Movie Database (TMDB); this product uses the TMDB API but is not endorsed or certified by TMDB.
- Listening data via Spotify.
The source is public so others can read, learn from, and borrow ideas. The written content, design, and personal data are © Khaled Waleed; please don't republish them wholesale or pass the site off as your own. Bundled paintings are public domain; third-party assets belong to their respective owners.