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 outputreal logic
build
yield
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 filingsample data
a toggle sends
Dinner▾412 kcal
Cheeseburger Baked Potato Boats
file into…
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 cardrecreated 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.