A self-updating, read-only sweepstake tracker. You edit one file
(results.json); GitHub rebuilds and redeploys the public site
automatically.
results.json ──(edit + push)──▶ GitHub Action runs build.mjs
──▶ index.html ──▶ GitHub Pages (public)
results.json— the only file you ever edit. Allocations, scores, prizes.build.mjs— computes standings, the 8-best-third-placed cut, prize owners, and bakes everything intoindex.html. You never edit this.template.html— the look of the site. You never edit this (unless you want to restyle).index.html— generated output. Don't edit it by hand; it gets overwritten on every build.
Viewers only ever see index.html. They cannot change anything — all data is
baked in at build time.
- Create a new GitHub repo, push these files to the
mainbranch. - Repo Settings → Pages → Build and deployment → Source: GitHub Actions.
- Push any change (or run the workflow manually: Actions → Build & Deploy → Run workflow).
- Your public URL appears under Settings → Pages. Share that link.
Edit results.json directly in GitHub's web UI (works on a phone): open the
file → pencil icon → edit → Commit. The site rebuilds in about one
minute.
Enter a score: find the fixture, set homeScore/awayScore to integers.
{ "group": "C", "date": "2026-06-13", "home": "Brazil", "away": "Morocco",
"homeScore": 2, "awayScore": 0 }Leave both as null for unplayed games. Standings, GD, and qualification
update automatically.
Set allocations: under allocations, map each person to their teams
using the exact team names from the groups block.
"allocations": {
"Dave": ["Brazil", "Japan", "Norway", "Panama"],
"Sam": ["England", "Spain", "Curaçao", "Iraq"]
}Award a prize: put the responsible team in the prize's team field.
The owner is resolved automatically. If nobody owns that team, it shows as
unowned (by design).
"firstHatTrick": {
"label": "First Hat-trick of Tournament",
"team": "Argentina",
"note": "Messi vs Algeria"
}Resolve a tie the build can't: if two teams finish equal on points, goal difference and goals scored, the site shows a red warning and the Action log lists it. The engine can't apply FIFA's later tiebreakers (team conduct, FIFA ranking), so you decide the order:
"tiebreakOverrides": {
"C": ["Brazil", "Scotland", "Haiti", "Morocco"]
}List all four teams top-to-bottom. The warning disappears on the next build.
Knockout stage: fill the knockout block as matches happen. Example:
"roundOf32": [
{
"home": "Brazil",
"away": "Switzerland",
"homeScore": 2,
"awayScore": 1,
"winner": "Brazil"
}
],
"final": {
"home": "Brazil",
"away": "France",
"homeScore": 1,
"awayScore": 0,
"winner": "Brazil"
}Setting a winner marks the loser eliminated on the Pool board.
node build.mjs && open index.html- Group eliminations only finalise on the Pool board once all 12 groups are complete, because the 8-best-third-placed cut needs every group's final table. Before that, only knockout losers are marked out.
- Fixtures are pre-seeded with correct groups and dates. Kick-off times aren't shown (date only) — add them to the template if wanted.
- First-red-card / first-hat-trick are manual: no free data source exposes them reliably.