Personal site & journal for Robin van der Vleuten — robinvdvleuten.nl. Static site built with Eleventy (11ty) v2 + Tailwind CSS v3, served by Caddy on Fly.io.
bashnpm install # install deps (Node 22 LTS; CI uses `npm ci`)npm start # dev: clean + eleventy --serve + tailwind --watch (use this)npm run build # production build → _site/ (clean + build:html + build:css)npm run build:html # eleventy onlynpm run build:css # tailwind only (minified)npm run clean # rm -rf _site
There is no test suite, linter, or typecheck. "Verifying" a change means running npm start and checking the rendered page, or npm run build and confirming it completes without errors.
Tailwind scans the built HTML in
_site/(seetailwind.config.jscontent), not the source templates. So CSS only picks up classes after Eleventy has written HTML.npm startruns both watchers in parallel, which handles this — but a one-offbuild:cssagainst a stale/empty_sitewill purge classes. Alwaysbuild:htmlbefore (or alongside)build:css.
Eleventy reads from the repo root and writes to _site/. Key config in .eleventy.js:
.njk/.html) and Markdown. Layouts use Nunjucks blocks.public/ → site root; the self-hosted Newsreader .woff2 files are copied from node_modules/@fontsource-variable/newsreader → /fonts/.eleventy-plugin-shiki-twoslash (syntax highlighting, theme css-variables) and @11ty/eleventy-plugin-rss (feed helpers).formatDate (dayjs, see _filters/date.js), head, plus RSS date/url helpers.2026, and a paired <aside id="sn-undefined"> <span class="text-accent mr-1">Nºundefined</span>… </aside> block that renders an <aside> with markdown inline content.post (all journal entries, tagged via post/post.json) and postByYear (posts grouped by year, used by the journal index).| Path | Purpose |
|---|---|
_includes/layouts/ |
default.html (base: <head>, fonts, OG/Twitter meta, header/footer chrome), post.html, page.html, about.html |
_includes/header.html, footer.html |
Shared chrome; header does section-active highlighting off page.url |
_data/site.js |
Site name, url, and environment (from NODE_ENV) |
_data/strava.json, _data/webmentions/*.json |
Generated data — do not hand-edit (see Automation) |
_filters/date.js |
dayjs formatDate filter |
post/*.md |
Journal entries. Filename YYYY-MM-DD-slug.md; date is parsed from the filename |
post/post.json |
Directory data file: applies post tag, layouts/post.html, and permalink: post/{slug}/ to every entry |
index.html, journal.html, about.md, contact.md, 404.md |
Top-level pages |
css/index.css |
Tailwind entry + @font-face declarations + custom prose/post-list styles |
feed.njk, sitemap.njk, robots.njk |
Generated XML/txt outputs |
public/ |
Static assets (favicon, images) copied verbatim |
scripts/ |
Node scripts run by GitHub Actions, not the build |
infra/ |
Terraform (DNS, Fly app) — gitignored from the build via .eleventyignore, edit only when changing infra |
.editorconfig). Markdown keeps trailing whitespace; the Caddyfile uses tabs.[font-weight:550], [text-wrap:balance]) are common and fine. Reusable component styles (.prose, .post-list, .display-title) live in css/index.css.tailwind.config.js): colors paper (#fbfaf7), ink (#211e1a), accent (#a33a24 / accent-dark); body font is the serif stack (Newsreader → Charter → Georgia). The aesthetic is deliberately literary/minimal — a single max-w-[42rem] column, serif type, burnt-sienna accent. Preserve this direction and keep existing copy intact unless asked otherwise.Create post/YYYY-MM-DD-title-slug.md with front matter:
markdown---title: Your Title Heredescription: One-line summary used for <meta description> and OG/Twitter cards.---Body in Markdown. Use ```lang fenced blocks for highlighted code (shiki-twoslash).
Tag, layout, and permalink are inherited from post/post.json — don't repeat them. The date comes from the filename. Posts auto-appear in the journal index, RSS feed, and sitemap.
main triggers .github/workflows/ci.yml → flyctl deploy --remote-only. The Dockerfile builds Caddy (with the transform-encoder plugin) + runs the Eleventy production build, then serves _site/ via .fly/Caddyfile. No manual deploy step needed.scripts/strava.mjs → _data/strava.json): refreshed by the Strava workflow on a Zapier repository_dispatch; auto-commits the JSON.scripts/webmentions.mjs → _data/webmentions/*.json): refreshed every 4 hours by the Webmentions workflow; auto-commits the JSON.Treat _data/strava.json and _data/webmentions/ as machine-generated — they're overwritten by CI, so don't edit them by hand.
_site/, node_modules/, infra/, and .envrc* are gitignored; _site/ and node_modules/ are build artifacts — never commit them.site.environment === "production" (i.e. NODE_ENV=production, set in the Dockerfile). The analytics script won't appear locally — that's expected.dependencies, never devDependencies. The Dockerfile builds with NODE_ENV=production + npm ci, which omits devDependencies — so a dev-only dep is silently absent in prod. This bites two ways: a require() in .eleventy.js throws and fails the build, or a passthrough-copied asset (e.g. the @fontsource-variable/newsreader fonts) just never ships and 404s on the live site while building fine locally. There is intentionally no devDependencies section — use npm install --save-prod, not --save-dev.package.json declares engines.node >= 22. Node 18 was EOL (April 2025); the bump also lifted a require(ESM) constraint. The Dockerfile's node:/alpine: tags are paired (Node 22 has no alpine3.17 variant) — bump ALPINE_VERSION and NODE_VERSION together..eleventy.js is CommonJS. Node 22 can require() ESM, but to stay friction-free, deps that are require()d in the config should still ship CommonJS — @11ty/eleventy-plugin-rss is pinned to ^1.2.0 (we only use its helper functions; v3 is ESM-only and unnecessary).