Open source · fork

Mealie fork

A fork of Mealie, the self-hosted recipe manager (13.7k stars upstream). I moved my recipes off a commercial app whose search only read titles and that had no nutrition data. I build the images, run them at home, and send generic fixes upstream: one is merged and credited in v3.27.0.

  • Python
  • Vue
  • Docker
  • CI/CD
  • Supply chain
  • Open source
v3.27.0
my fix merged and credited upstream
6/6
TLS tests pass after fixing my own scraper
147 → 100
Python packages in my image, no dev tools left in it
77
of my 274 recipes in 2+ categories, protected by the fix
Upstream

The bug behind PR #7881

In one line: recipes with servings but no yield published a yield of 0 in their schema.org data, and 0 is the denominator for anything per serving.

Mealie emits schema.org JSON-LD for every recipe, which is how search engines, scrapers and nutrition trackers read it. The yield string was built from the yield quantity and text, and a validator turns an unset quantity into zero. So a servings-only recipe emitted a bare "0.0": not missing, zero.

mealie · schema.org output real logic
build
yield
4
 

With a quantity-and-text yield it was only cosmetic ("8.0 slices"), which is why it survived: the case that broke the math only hit servings-only recipes, in metadata nobody reads by eye. The fix covers both, with parametrized tests over every yield shape. A maintainer tightened the fallback in review so a text-only yield never gets servings put in front of it, merged it on 09-16, and it shipped in v3.27.0 the next day with a first-contributor credit.

Security

A finding in my own code

In one line: the recipe scraper my fork carries fetched pages with TLS certificate checks turned off. I found it, fixed it, and proved it with tests before trusting the image.

fork scraper · fixed 10-23

TLS verification was off in 3 places

Anyone on the network path could have fed the recipe parser whatever they liked.

found
A review of my fork's scraper: page fetch, short-link resolve and an embed lookup all passed verify=False. Upstream Mealie does not have this; it came in with code my fork added.
before
Test sites with an expired, self-signed, wrong-host and untrusted-root certificate were all fetched.
fix
One transport verifies by default (certifi). The only exception is an exact host list set by the admin, validated at startup, and every skip is logged. A failed check stops the fetch with a clear message instead of retrying without verification.
proof
4 new test files. On the deployed image: all 4 bad certificates refused, a normal site and a plain-HTTP site fetched: 6/6 pass, accepted by image digest.
Supply chain

A smaller image, accepted by digest

In one line: the image I run carries only what the app needs, is scanned daily, and is never trusted until a script checks the exact digest that is running.

  • Slimmed: 147 Python packages became 100, and the running image has no pip, pytest, ruff or mypy in it (checked 10-31). High-severity CVEs went 52 → 44 from that alone.
  • Patched: a dependency bump on 10-29 cleared every critical CVE in the image that had a fixed version available, verified by the daily scan.
  • Accepted, not assumed: each new image goes through an acceptance script: digest match, health, login, the public page, and the change's own tests. The previous image stays tagged for rollback.
  • Built in the open: every push to the fork builds and publishes a public image to GitHub's registry. The one running now was built 10-29 from commit 7334ca0d.
Data integrity

The one that could quietly empty my cookbooks

In one line: the update call replaces a recipe's whole category list, so sending only the tapped category silently removes all the others. 77 of my 274 recipes sit in more than one category.

Tapping the pill on a card files the recipe into a cookbook. The obvious implementation sends the category you tapped. 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

     

    My real cookbook names, minus one I am not putting on a portfolio. The fix is four lines: read the current list, merge, send all of it. The naive version works on any recipe in one folder, and most hand-written tests start with a recipe in one folder. Checked on the wire before I trusted it.

    The redesign

    A shape problem, not a colour problem

    In one line: the theme set colours but never component defaults, so every card kept stock Material shapes under any palette.

    Adding defaults in that one file restyled every page: recipes, search, cookbooks, meal planner, settings. Small diff, easy to rebase, because upstream rarely touches it. Calories on the card needed a separate backend change that put nutrition in the list response.

    mealie · recipe card recreated in css
    recipe card

    Release engineering

    The CI that went green and built nothing

    In one line: upstream's publish job only runs on upstream's repo, so on a fork it is skipped and the run still shows green.

    • The silent skip: if: github.repository == 'mealie-recipes/mealie' is false on a fork. The job is skipped, the run reports success, and no image is built. Repointed at the fork.
    • The rest: a paid build service I have no token for, swapped for stock buildx with layer caching; publishing to my own namespace; the image name lowercased because the registry rejects capitals.
    • The trap: the GitHub CLI in a fork defaults to the parent repo. Unless every command names the fork, you read upstream's CI and conclude your build never ran.

    what it demonstrates

    Python and Vue across the stack, upstream contribution through maintainer review, finding and fixing a security bug in my own code with tests, container supply-chain hygiene (slim runtime image, daily Trivy, acceptance by digest with rollback), and CI/CD on a fork.

    ethan@justice: ~