Gallery
Static photo gallery for logging meals and food memories.
The site is based on the HTML5 UP Lens template. The front-end remains a mostly static HTML/CSS/JavaScript site, and the local Node server now also exposes a tiny rankings sync API for shared Elo persistence.
Repo Layout
templates/index.html: source template for the main gallery pagetemplates/rankings.html: source template for the rankings pageindex.html: generated static gallery pagerankings.html: generated static rankings pageassets/: site CSS, JavaScript, fonts, and audioimages/fulls/: full-size gallery imagesimages/thumbs/: gallery thumbnailsdata/meals.json: source of truth for gallery entriesdata/elo.json: Elo ratings, record totals, and ranking settingsscripts/build.js: renders static pages from templates and datascripts/check.js: validates data, image assets, and generated pagesscripts/generate-thumbnails.js: regenerates thumbnails from the full-size imagesscripts/ingest-meal.js: ingests a new meal image and metadata in one commandscripts/serve.js: serves the generated site and the rankings sync APIscripts/lib/elo.js: validates and syncs Elo data against the meal listscripts/lib/rankings-state.js: normalizes and persists the shared rankings statepackage.json: minimal Node build entrypoint
Run Locally
Install dependencies:
npm install
Build the site and validate the generated output:
npm run build
Serve it locally:
npm run serve
Then open http://127.0.0.1:4321.
By default, rankings sync state is written to .runtime/rankings-state.json.
Override that path with RANKINGS_STATE_PATH=/absolute/path/to/rankings-state.json.
If you want a single command that builds and serves, run:
npm start
To validate the repo state without rebuilding thumbnails or pages, run:
npm run check
Content Workflow
Gallery entries live in data/meals.json, and the build generates both index.html and rankings.html from the template and data files.
After editing content or templates, rebuild the site with:
npm run build
The gallery build keeps the existing Lens thumbnail markup intact, so the current client-side viewer code continues to work.
To ingest a new meal image and update the site in one command, run:
npm run ingest -- --image /path/to/photo.jpg --title "meal title" --description "notes"
Optional ingestion flags:
--position "left center"sets the viewer image alignment--focus-x 0.35 --focus-y 0.45sets the thumbnail crop focal point
If you only need to regenerate thumbnails, run:
npm run build:thumbs
To force a full thumbnail rebuild, run:
npm run build:thumbs:force
Rankings Data
data/elo.json stores the seed rating, Elo kFactor, and a win-loss record for each meal.
The page build keeps this file aligned with data/meals.json, so new meals automatically appear in rankings.html with the default seed rating.
The interactive voting flow on rankings.html now prefers the same-origin API exposed by scripts/serve.js:
GET /api/rankings: load the shared rankings statePOST /api/rankings/vote: apply one head-to-head result on the serverPOST /api/rankings/reset: reset the shared board back to the seeded state
The server persists the shared board to .runtime/rankings-state.json by default, or to RANKINGS_STATE_PATH if you set it.
That makes rankings persist across reloads, sessions, browsers, and devices as long as they are hitting the same deployed site.
If the API is unavailable, the page falls back to browser localStorage.
In that fallback mode, votes still persist across reloads in the same browser profile, but they do not sync across browsers or devices.
Use the reset button on the rankings page if you want to clear the current saved board and go back to the seeded state.
Deployment Notes
For Docker/VPS deployment, mount a persistent volume and point RANKINGS_STATE_PATH at it so rankings survive container rebuilds and restarts.
Example:
RANKINGS_STATE_PATH=/data/rankings-state.json npm start
In a containerized setup, mount /data as a named volume or bind mount.
If you reverse-proxy the app through Caddy on the same domain, the rankings page will use the shared API automatically with no extra CORS setup.
Image Conventions
- Full-size images and thumbnails share the same numeric ID
- Full-size images live at
images/fulls/<id>.jpg - Thumbnails live at
images/thumbs/<id>.jpg positioncontrols the full-screen viewer image alignmentthumbnail.focusoptionally overrides the default center crop for generated thumbnails
Thumbnail Focus
Thumbnails are generated from images/fulls with sharp at 240x320.
The generator auto-rotates images using EXIF orientation, skips unchanged files by default, and removes stale thumbnail .jpg files that no longer map to a meal entry.
For images that should crop away from the center, add optional thumbnail focus metadata to the meal entry:
{
"id": "34",
"title": "example",
"description": "example",
"thumbnail": {
"focus": {
"x": 0.35,
"y": 0.45
}
}
}
The x and y values are normalized from 0 to 1, where 0.5, 0.5 is the center of the image.