Skip to content

Repository files navigation

khaledwaleed.com

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

SvelteKit Svelte 5 Tailwind CSS TypeScript pnpm Cloudflare Pages


The idea

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.

Tech stack

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)

Getting started

Prerequisites

Install & run

pnpm install
pnpm dev          # start the dev server
pnpm dev --open   # …and open it in the browser

Other scripts

pnpm 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 --fix

CI (GitHub Actions) runs lint → check → test → build on every push and PR to main.

Environment variables

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.

Project structure

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.

Pages

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

Data & content tooling

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 .svx file under src/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 paintings map in site.ts, map it in roomForPath, add a [data-room] palette in app.css, drop the image in static/paintings/, and run generate-painting-sizes.ts.

Deployment

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

Credits

  • Paintings are in the public domain; each is credited (artist, year, museum) in src/lib/site.ts and 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.

License

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.

About

My personal website.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages