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 Playfair Display + Lato 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 PocketBase 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)
│ ├─ pocketbase.ts # PocketBase client + Film record types (ratings live in PB)
│ ├─ 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 PocketBase, 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, PocketBase-backed): sign in, search TMDB, pick a title, set rating / watch dates / notes. A denormalized TMDB snapshot (title, year, directors, poster, runtime, genres) is written to PocketBase at save time, 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 deploys to Cloudflare Pages. The static pages are served from the edge cache; /music runs as a Pages Function. The adapter's routes.exclude list in svelte.config.js keeps the generated _routes.json under Cloudflare's 100-rule limit by collapsing asset folders into wildcards.
- 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.