Personal website of Chris McLeod, built with Eleventy and deployed to Bunny.
Node.js 22.17 or later (.nvmrc pins the version CI uses). Install with npm ci.
| Command | What it does |
|---|---|
npm start |
Development server on http://localhost:3000 |
npm run build |
Build the site into dist/, then index it for search with Pagefind |
npm test |
Run the unit tests (*.test.js, with Node's built-in test runner) |
npm run check |
Check dist/ for known classes of bugs (see helpers/check-build.js) |
npm run data:sync |
Refresh the Paintslam snapshot, data/hobby.json, from mage |
npm run images:sync |
Refresh the image manifest, data/images.json |
npm run og |
Generate missing social preview images (og:force regenerates all) |
npm run new:post, new:note, new:bookmark, new:like |
Create a content file |
The two sync commands read MAGE_API_TOKEN (and optionally MAGE_URL) from the environment or from a .env file; see .env-sample. A build never needs the network or any secret.
| Folder | Content | Published at |
|---|---|---|
src/posts/ |
Blog posts | /blog/<title>/ |
src/notes/ |
Short notes, many posted through mage's micropub endpoint | /notes/<date>/ |
src/bookmarks/ |
Links | /bookmarks/<title>/ |
src/likes/ |
Likes | not published |
Front matter keys with special meaning:
bookmarkOf: the bookmarked URL (bookmarks).likeOf: the liked URL (likes).redirectFrom: old paths (a string or a list) that should forward to this page; each gets a small redirect page.ogImage: the social preview image insrc/assets/images/social-preview/;npm run ogfills it in. Its name records the post's slug, so after a post's title changesnpm run ogmakes a new image, deletes the old one, and adds the old URL toredirectFrom(seedocs/adr/0002-preview-image-name-records-the-slug.md). A renamed bookmark needs its old URL added toredirectFromby hand.
Post and bookmark URLs, file names and preview image names all come from the title, through config/identity/; the terms are defined in CONTEXT.md.
atUri: the post's AT Protocol record; Sequoia writes it.
Feeds are defined once, in src/_data/feeds.js, and rendered by the templates in src/feeds/.
- Upload the image through mage. mage stores the original on the CDN (
https://assets.chrismcleod.dev) and resized copies in several widths and formats. - Write ordinary Markdown with the CDN URL:
. An image on its own line becomes a<figure>, and its title becomes the caption. - Run
npm run images:sync, or let CI do it. It records the image's size and which resized copies exist indata/images.json, and the build turns the image into responsive<picture>markup from that. Nothing is downloaded or resized at build time.
If the build warns [images] N images have no manifest entry; run npm run images:sync, those images are shown as a plain <img> of the original until the manifest is synced.
Some old images share a file name with another image in a different folder. The CDN keeps resized copies in one flat folder, so such images cannot have their own copies; they are always shown as their original. images:sync lists them.
Templates can use {{ src | picture(alt, className) }}, and the {% image src, alt, caption, sizes, classes, lazy %} shortcode remains for the few images that need a class or eager loading.
src/assets/css/global.css is the only stylesheet the build writes; PostCSS inlines its imports (@import-glob takes every file in a folder, in name order), then prefixes and minifies the result. The design tokens in src/_data/designTokens/*.json become custom properties such as --space-s and --size-step-1, generated by config/template-languages/design-tokens.js where global/design-tokens.css says @design-tokens;. Single-purpose classes such as .sr-only and .text-center are plain CSS in utility-classes.css, imported last so that they win.
data/ holds committed copies of external data: hobby.json (Paintslam entries from mage) and images.json (the image manifest). The sync commands only replace a snapshot with a valid response, write atomically, and never delete image entries; if a sync fails, the build uses the committed snapshot. To undo a bad sync, run git checkout -- data/.
.github/workflows/check.yml builds and checks every pull request, with no secrets.
.github/workflows/deploy-to-bunny.yml runs on pushes to develop, on manual runs, and on the function_trigger repository dispatch that a Paintslam change sends. In order, it:
- syncs
data/hobby.json(a failure stops a dispatch-triggered run, and is only a warning otherwise) anddata/images.json; - generates social preview images, builds, and runs
npm run check; - publishes new and changed posts to the AT Protocol with Sequoia (
npx sequoia publish), which writesatUriinto post front matter; - builds again and checks again, so pages include the new
atUrivalues; - commits the generated state (
.sequoia-state.json, posts, preview images,data/) back todevelopwith[skip ci]; - uploads
dist/to Bunny storage.
If a run fails after step 3, re-run it. Posts already published are recorded in .sequoia-state.json only if step 5 ran; if it did not, Sequoia will see them as new again, so check the AT Protocol records first and, if necessary, commit the state file by hand.
Sequoia republishes a post whenever its file changes in any way, front matter included, so bulk edits to src/posts/ update many records at once (updates do not post to Bluesky). Preview with npx sequoia publish --dry-run.
The Bunny pull zone has settings that are not in this repository:
- Edge rules sending
/.well-known/webfinger*and/.well-known/host-meta*tohttps://fed.brid.gy/...(Bridgy Fed), and/feed/to/follow/blog/feed.atom. - Response headers:
Content-Security-Policy: upgrade-insecure-requests; block-all-mixed-content;X-Content-Type-Options: nosniffX-Frame-Options: DENYReferrer-Policy: strict-origin-when-cross-originPermissions-Policy: autoplay=(), camera=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), publickey-credentials-get=()
src/assets/scripts/sequoia-comments.js and sequoia-subscribe.js are web components from the Sequoia project, copied from sequoia-cli 0.5.7 (node_modules/sequoia-cli/dist/components/) and extended with CSS custom properties for the button styles. To update them, copy the new upstream files and re-apply those additions rather than refactoring them.
Based on the excellent Eleventy Excellent starter by Lene Saile.
All I've done is tweak a few minor knobs and dials here and there, to get things "just so" for my tastes. If you want a well thought-out and robust Eleventy starter, I recommend it.