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 Playfair Display + Lato 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 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.

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)
│  ├─ 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.

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

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

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.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages