Open source · fork
Mealie fork
A fork of Mealie, the self-hosted recipe manager (12.7k stars upstream), that I build, deploy, and extend for a nutrition-tracking pipeline. I ship the images, run them in the homelab, and send the generic fixes back upstream.
- PR #7881
- open upstream
- 4
- CI fixes to build at all
- nightly
- images to my registry
- 1
- upstream contribution
It looked like this
Two rounds of theme work went in and it still looked dated, because I was changing colours and the problem was shape. Flip between them.
Why recoloring never worked
The theme plugin set colors and never set component defaults, so every card, button and chip in the app kept rendering stock Material underneath whatever palette I handed it. Elevation shadows, 4px radii, filled chips. You cannot recolor your way out of a shape problem.
Adding defaults in that one file restyled every page in the app: recipes, search, cookbooks, meal planner, settings. Small diff, rebase-safe, because upstream barely touches it. The calories only exist at all because of a separate backend change that put nutrition in the list payload.
The one that could quietly destroy the library
Tapping the pill on a card files the recipe into a cookbook. Recipes are multi-membership on purpose: one can live in Dinner and DELICIOUS and Quickies at once. The trap is that the patch endpoint replaces the category list with exactly what you send it.
So the obvious implementation, send the category the user just tapped, is catastrophic and completely silent. Tap a category below and watch the shelf.
Cheeseburger Baked Potato Boats
cookbooks this recipe is actually in
His real cookbook set, minus one whose name I am not putting on a portfolio. The read-modify-write is the whole fix and it is four lines, which is exactly why it is the kind of thing that ships wrong: the naive version works perfectly on any recipe that is only in one folder, and every test you would write by hand starts with a recipe in one folder. Verified on the wire before I trusted it.
The bug behind PR #7881
Mealie emits schema.org JSON-LD for every recipe, which is how any consumer
(search engines, scrapers, nutrition trackers) reads it. The yield string was
built from the yield quantity and the yield text, and a
validator coerces an unset quantity to zero. So a recipe with servings but no
explicit yield emitted a bare "0.0". Not missing. Zero — and it
is the denominator for anything per-serving.
Switch to a quantity-and-text yield and it degrades from destructive to
merely wrong: "8.0 slices" instead of "8 slices".
That's why it survived. The visible case looked like a cosmetic trailing
zero, and the case that actually poisoned the math only fired for
servings-only recipes, in emitted metadata nobody reads by eye. The fix
handles both and ships with parametrized tests over every yield shape.
Why the fork exists
I'm moving roughly 300 recipes off a commercial app with two structural problems. Its search indexes the title and nothing else, so searching "Worcestershire" returns zero results for the recipes that call for it. And there is no nutrition field anywhere in its data model, which makes the one thing I actually want impossible rather than merely missing.
Same model as my Android fork: own the code so "I wish it had X" becomes something you just do. Upstream the generic, fork-carry the opinionated. The yield fix is broadly useful, so it goes upstream and gets maintained forever at no cost to me. The macro-first UI is mine and upstream would never take it, which is fine — it stays small and rebasable.
The CI that went green and built nothing
Upstream's pipeline is built for upstream, and none of it works on a fork. Four things had to be fixed; only the first was dangerous.
- The publish job was gated to the upstream repo.
if: github.repository == 'mealie-recipes/mealie'. On a fork that is false, so the job is skipped, the run reports green, and no image is ever built. A pipeline that fails loudly costs you ten minutes. One that succeeds while doing nothing costs you however long it takes to notice. - It built through a paid service I have no token for; swapped for stock buildx with layer caching. It pushed to the maintainer's namespace; I publish to my own. The registry rejects uppercase image names and my username has capitals, so the repo name gets lowercased explicitly.
- A related trap: the CLI in that repo defaults to the parent, not the fork. Point every command at the fork explicitly or you're reading upstream's CI and concluding your build never ran.
what it demonstrates
CI/CD surgery, Docker and GHCR pipelines, Python and Vue across the full stack, upstream open-source contribution, and the discipline of running what you build.