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.

  • CI/CD
  • Docker
  • GHCR
  • Python
  • Vue
  • Open source
PR #7881
open upstream
4
CI fixes to build at all
nightly
images to my registry
1
upstream contribution
The redesign

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.

mealie · recipe card recreated in css
recipe card

Root cause

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.

Data integrity

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.

mealie · category filing sample data
a toggle sends
Dinner 412 kcal

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.

    Upstream

    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.

    mealie · schema.org output real logic
    build yield
    4
     

    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.

    Motivation

    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.

    Release engineering

    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.