Some data does not belong to one household but to everyone who uses MealieShopper: a list kept up together, a mapping someone already worked out once. It belongs in one fixed place, at the same address for everyone, where nothing is added without someone having looked at it. This repository is that place. You send a file as a pull request; a check shows what changes on the site, including what is inside a zip; the owner approves it, and then it goes online by itself.
.json or .zip: one document, or a zip with many documents at once, straight from the browser.publicatie and on data.mealieshopper.nl, with a catalogue in index.json.schemas/ decides what a collection accepts.Everything is at https://data.mealieshopper.nl. Start with the catalogue:
curl https://data.mealieshopper.nl/index.json
{
"site": "https://data.mealieshopper.nl",
"generated_at": "2026-11-07T10:00:00Z",
"commit": "…",
"collections": [
{
"name": "recepten",
"title": "Recepten",
"description": "…",
"schema": "https://data.mealieshopper.nl/schemas/recepten.schema.json",
"files": [
{ "path": "recepten/soepen.json", "url": "https://data.mealieshopper.nl/recepten/soepen.json",
"bytes": 2048, "sha256": "…", "items": 12 }
]
}
]
}
https://data.mealieshopper.nl/<collection>/<file>.json.items is the number of items when the document is a list, otherwise null.sha256 is that of the file as served. Compare it when you need to be sure you have a particular version.commit and generated_at belong to the last change to the data.Access-Control-Allow-Origin: *, so a web app can fetch everything directly.You need a GitHub account and nothing else.
data/, for example data/recepten/. If it does not exist yet, create it in your pull request (a new collection preferably comes with a schema, see schemas/)..json or .zip file in.| Rule | Why |
|---|---|
Only .json, or a .zip with only .json |
What is in it must be checkable. What zip tools add on their own (__MACOSX/, .DS_Store) is skipped. |
A document belongs to a collection: data/<collection>/…/<name>.json |
The first folder is the collection. |
Collection names: lowercase letters, digits and - (not schemas). File names: letters, digits, ., - and _, no spaces |
Every path is also a web address. |
A zip unpacks where it is: data/recepten/bundel.zip with a.json inside becomes recepten/a.json |
That is how you send many files at once. A zip directly in data/ carries the collections as folders. |
| Every path on the site comes from one file, also when case is ignored | Two sources for the same address cannot both win. |
Strict JSON: UTF-8, no duplicate keys, no NaN, an object or a list at the top |
A duplicate key disappears without a trace. |
Matches schemas/<collection>.schema.json, when there is one |
That is how a collection decides what it accepts. |
No keys that suggest a secret (token, api_key, password, secret, cookie, …) |
Everything here is public. |
| At most 10 MB per document, 25 MB per zip, 100 MB unpacked per zip and 2000 files per zip | 25 MB is also GitHub’s limit for a file added in the browser. |
| Data and code not in one pull request | A submission cannot change the check that judges it. |
main with files in data/ (and possibly a schema in schemas/).main after the merge, with the rules and the code of main (not those of the pull request), and lists in its summary what changes on the site. The check Tests runs the tests.main: every document unpacked and formatted, the schemas, index.json and an overview page. It puts that as a new commit on the branch publicatie and deploys it to GitHub Pages, at data.mealieshopper.nl.main keeps the files as they were approved, a zip included. If you prefer loose files in main, unpack a zip with python scripts/datastore.py unpack data/<collection>/<file>.zip and send that as a pull request. Nothing changes on the site.
Once, by the owner, in the repository settings on GitHub.
main as the default branch. Create the branch main (if it does not exist yet) and select it under Settings → General → Default branch.data.mealieshopper.nl under Custom domain and turn on Enforce HTTPS. Pages for a private repository needs GitHub Pro or a paid plan; the site itself is always public.CNAME record for data.mealieshopper.nl pointing to helmerznl.github.io.main (Settings → Rules → Rulesets, or Branches):
CODEOWNERS);Do not protect the branch publicatie in a way that blocks github-actions[bot]: only the workflow writes to it.
GitHub does not let you approve your own pull request. When you send something yourself, merge as an administrator with Merge without waiting for requirements to be met, once the checks are green.
| Task | How |
|---|---|
| Approve a submission | Review the pull request, Files changed and the summary of Inzending controleren, then Review changes → Approve and merge |
| Take something off the site | A pull request that removes the file from data/ (or replaces the zip it is in) |
| Roll back a publication | Revert the pull request and merge the revert; the site follows main |
| Publish again | Actions → Publiceren → Run workflow |
| Check locally | python -m pip install -r requirements.txt, then python scripts/datastore.py check |
| Build the site locally | python scripts/datastore.py build --out _site and open _site/index.html |
GET https://data.mealieshopper.nl/index.json answers 200 with the catalogue.| Where | What it holds |
|---|---|
main, folder data/ |
The approved files, as they were sent |
main, folder schemas/ |
The JSON Schema per collection |
Branch publicatie |
The published site: everything unpacked and formatted, with index.json. Every publication is a commit |
| GitHub Pages | The same site, at data.mealieshopper.nl |
The repository is the backup: every submission and every publication is in the git history. publicatie and the site can always be rebuilt from main (Run workflow). Make a copy with git clone --mirror.
flowchart LR
I[Submitter] -->|pull request<br/>.json or .zip| PR[Pull request into main]
PR --> C[Inzending controleren<br/>and Tests]
C -->|summary| E[Owner approves<br/>and merges]
E --> M[(main: data/, schemas/)]
M --> P[Publiceren]
P --> B[(branch publicatie)]
P --> G[GitHub Pages<br/>data.mealieshopper.nl]
G --> A[MealieShopper and<br/>other users]
| Part | Purpose |
|---|---|
scripts/datastore.py |
check, build and unpack: every rule lives here. Python with only jsonschema added |
inzending.yml |
Inzending controleren, with main’s code (pull_request_target); runs nothing the pull request brings in |
controleren.yml |
Tests, with the pull request’s code |
publiceren.yml |
Build, put on publicatie and deploy to GitHub Pages |
site/ |
The overview page and the logo |
Sending data is described above. A change to the code (the check, the site, the workflows) goes in its own pull request into main, never together with data. The working conventions for people and AI assistants are in AGENTS.md.
python -m pip install -r requirements.txt
python -m unittest discover -s tests
python scripts/datastore.py check
main’s code, not the pull request’s, and only reads the submitted files. A zip with .., an absolute path, a symbolic link or a password is refused, as is a zip that unpacks too large.MealieShopper is free. Find it useful? A coffee keeps development going.
No license has been chosen yet for the data or the code: all rights reserved.