A vegan recipe blog whose recipes are written and illustrated by AI, once a day, without a human in the loop. Built with Eleventy v3 and served as static files from GitHub Pages.
Live site: recipe.polente.de
Every recipe on the site is generated — text, image and all — so treat the nutrition figures and allergen information as decoration rather than fact. The imprint says the same thing at more length.
A cronjob runs once a day, prompts Gemini for a recipe and a matching image, writes a
markdown file plus a PNG into src/, and pushes. The push triggers the GitHub Actions
workflow, which builds the site and deploys it. See FLOWCHART.md for the
diagram.
There are currently 226 recipes, attributed to four fictional chefs (Emily, Hiroshi,
Isabella and Nia) defined in src/_11ty/data/chefs.js. The
chef is the last, capitalised entry in a post's tags.
Requires Node v24.18.0 (see .nvmrc). The hard floor is Node 22, which
@11ty/eleventy-img v7 needs; 24 is what CI builds with.
npm install
npm run serve # Eleventy + PostCSS in parallel, with live reload, on localhost:8080| Script | What it does |
|---|---|
npm run serve |
Dev server with watch and live reload |
npm run build |
Full production build into dist/ |
npm run clean |
Delete dist/ |
npx prettier --write . |
Format |
npm run build runs four steps: Eleventy and PostCSS in parallel, then
Critters to inline critical CSS, then
Pagefind to build the search index. The first build is slow
because @11ty/eleventy-img generates AVIF, WebP and JPEG at five widths for every recipe
photo; later builds reuse what is already in dist/img/.
src/
├── _11ty/ Eleventy extensions: collections, filters, shortcodes,
│ transforms, global data (meta.js, chefs.js), markdown-it config
├── _includes/ Nunjucks layouts and partials
├── _styles/ PostCSS sources — config, variables, utilities, base,
│ layout, components
├── content/ Pages, and posts/ with one markdown file per recipe
├── media/ Recipe images (source resolution)
├── static/ Passthrough: fonts, favicons, JS, robots.txt
└── site.css PostCSS entry point
CLAUDE.md documents the architecture in more depth — the filters, the transforms, the consent model and the microformats markup.
- Structured data.
extractRecipeDataparses the rendered HTML of each post to pull out ingredients, numbered steps, times, yield and nutrition, and emits schema.orgRecipeJSON-LD from it.Person,BreadcrumbListandWebSiteJSON-LD are emitted too. - Cook mode. A full-screen step-by-step view with tickable ingredients, tap-to-start
timers parsed out of the instruction text, and a screen wake lock
(
src/static/js/cook-mode.js). - Print modes. Print the whole recipe, or just a shopping list of the ingredients you have not already ticked off.
- Client-side search over every recipe, with chef and cuisine filters, via Pagefind.
- Webmentions, opt-in — see below.
- Microformats2. Posts are
h-entrywith ap-author h-card; listings areh-feed; chef pages areh-card. This is what lets other IndieWeb sites render a link to a recipe as a real post rather than a bare URL.
The site sets no cookies, loads no third-party resources and runs no analytics by default. There is exactly one exception, and it is opt-in:
Recipes can be replied to from elsewhere on the web via
Webmention, and those responses are collected by
webmention.io. Fetching them means the reader's browser contacts
that service, so a banner asks first. Nothing is requested until they accept, and declining
removes the responses section from the page entirely. The answer is stored in a single
first-party cookie, wm-consent.
The renderer in src/static/js/webmentions.js is
deliberately not webmention.io's own embed script, because it applies rules that script
does not:
- Likes, reposts, bookmarks and RSVPs are shown as a facepile — picture, link and name —
deduplicated per person, counted by distinct person, and capped at 24 faces plus a
+Nchip so a syndicated post cannot fire hundreds of image requests. - Profile pictures are requested only when the host is exactly
avatars.webmention.io, webmention.io's own proxy. Roughly a tenth ofauthor.photovalues point somewhere else (a personal domain,cdn.bsky.app); those are dropped and a CSS initial stands in. That allowlist is the whole reason "exactly one third party" stays literally true. - Replies show a name, a date and plain text.
- Reply content is inserted as text, never as markup, so a reply cannot make the reader's browser contact a third site.
The reasoning follows Alex Hyett's
Opting for privacy in webmentions.
src/content/privacy.md is the reader-facing version and should
be kept in sync with the code.
Pushing to main runs .github/workflows/jekyll-gh-pages.yml,
which builds and publishes dist/ to GitHub Pages. The custom domain comes from the
CNAME file. Because Pages serves no custom response headers, anything
header-shaped has to be done in markup.
Once a deploy succeeds, a second job in the same workflow runs
scripts/post-to-mastodon.js, which announces any recipe
that was added in that push. The post is the recipe's introduction paragraph, truncated
on a sentence boundary to fit Mastodon's 500 characters alongside the title, three
hashtags and the URL. (Mastodon counts every URL as 23 characters whatever its length,
which is what makes the budget workable.)
It needs two repository settings, neither of which lives in the repo:
- secret
MASTODON— an access token from Preferences → Development → New application on the instance, scopewrite:statuses - variable
MASTODON_SERVER— e.g.https://hellinger.wtf/
Without the secret the job is skipped, so forks and local clones are unaffected.
What stops it double-posting, or posting the back catalogue, is a mastodon-syndicated
git tag: the script diffs from that tag to HEAD with --diff-filter=A and advances it
only as far as the last recipe that actually posted. A failed run, a Mastodon outage or a
deploy dropped by the pages concurrency group therefore leaves the tag alone and is
picked up by the next run. Seed it once before the first live run, or the script finds
no base and does nothing:
git tag -f mastodon-syndicated HEAD && git push -f origin mastodon-syndicatedTo see what would be posted without posting anything — no token required:
node scripts/post-to-mastodon.js --dry-run --since HEAD~10
node scripts/post-to-mastodon.js --dry-run --file src/content/posts/<a-recipe>.md--visibility direct posts for real but only to you, which is the safest smoke test.
Issues and pull requests are welcome, but note that src/content/posts/ and src/media/
are written by the daily job — changes there will be buried under new commits. Everything
else (templates, styles, build config, client-side JS) is hand-written and fair game.