Multilingual podiatry practice website (voorvoet.nl) built with SvelteKit 2 + Svelte 5.
Get productive in 10 minutes:
pnpm install # also compiles Paraglide i18n types
cp .env.example .env # fill in SMTP + Cap values (defaults work for local dev)
pnpm dev # http://localhost:5173
| Layer | Technology |
|---|---|
| Framework | SvelteKit 2 + Svelte 5 (runes) |
| Adapter | @sveltejs/adapter-node |
| i18n | Paraglide.js (nl, de, en — nl default) |
| Blog | mdsvex (Markdown in src/content/blog/{lang}/) |
| Forms | sveltekit-superforms + zod |
| Logging | pino (JSON in prod, pretty-print in dev) |
| Tests | Vitest (unit) + Playwright (E2E) |
| CI | GitHub Actions + Lighthouse CI |
| Deploy | Docker image on GHCR; TLS at upstream proxy |
# Install deps and compile i18n
pnpm install
# Copy env — defaults are safe for local dev
cp .env.example .env
# Start dev server with HMR
pnpm dev
| Command | Purpose |
|---|---|
pnpm dev |
Dev server at :5173 |
pnpm build |
Production build |
pnpm preview |
Serve production build at :4173 |
pnpm test |
Unit tests (Vitest) |
pnpm test:coverage |
Unit tests with coverage |
pnpm e2e |
E2E tests (needs pnpm preview running) |
pnpm lint |
ESLint — zero warnings |
pnpm format |
Prettier write |
pnpm format:check |
Prettier check |
pnpm check |
svelte-check TypeScript check |
pnpm paraglide:compile |
Regenerate i18n from messages/ |
pnpm version:sync |
Sync package.json from the VERSION file |
pnpm lighthouse |
Run Lighthouse CI |
src/
app.html SvelteKit HTML template
app.css Global CSS (tokens, @font-face)
app.d.ts TypeScript ambient declarations
hooks.server.ts Request ID, CSP, security headers, Umami tracking
lib/
data/ JSON/CSV data (reimbursements, pricing)
forms/ Form schemas + action handlers
i18n/ Route map, page meta, locale map
paraglide/ Generated by Paraglide (gitignored)
server/ Server-only modules: email, cap (CAPTCHA), umami, logger
styles/ Design token CSS
params/
lang.ts [lang=lang] param matcher
routes/
+layout.svelte
+page.server.ts Redirect / → /nl
[lang=lang]/ Language-prefixed pages
content/
blog/{lang}/ Markdown blog posts
legal/{lang}/ Privacy policy, terms of service
messages/ Paraglide i18n message files (nl.json, de.json, en.json)
project.inlang/ inlang project settings
docs/ DEPLOY, RUNBOOK, MIGRATION, CONTRIBUTING guides
docker-compose.yml App service (expects an upstream reverse proxy)
Dockerfile Multi-stage Node.js build
VERSION in the repo root is the single source of truth for the app version.
svelte.config.js and exposed to all client and server code as version from $app/environment (SvelteKit's kit.version.name).package.json's version field must match. Edit VERSION, then run pnpm version:sync to update package.json. CI runs pnpm version:check and fails if the two drift.import { version } from '$app/environment';
// version === content of VERSION file
See docs/RUNBOOK.md for the full walkthrough including frontmatter shape and image requirements.
Short version: create src/content/blog/{nl,de,en}/your-slug.md with the required frontmatter, then pnpm dev to preview.
The app ships as a Docker image published to GHCR by the docker-publish.yml workflow on every push to main. It is designed to run behind an upstream reverse proxy that terminates TLS and forwards to port 3000.
See docs/DEPLOY.md for env vars, proxy header configuration, and rollback procedure.
# Pull and run the latest image
docker compose pull && docker compose up -d
See docs/CONTRIBUTING.md for branching convention, pre-commit hooks, and PR template.
docker build -t voorvoet .
docker run -p 3000:3000 --env-file .env voorvoet
# Or via compose (builds from local Dockerfile with --build)
docker compose up --build
Source code in this repository is licensed under the Apache License 2.0.
Content under static/images/, static/documents/, src/content/, and the practice-specific copy in messages/ is owned by VoorVoet and is not covered by the Apache License — see NOTICE for the full carve-out.