diff --git a/README.md b/README.md index 8e8b692..4b93ab9 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,47 @@ The site is based on the HTML5 UP Lens template and currently ships as a plain s - `data/meals.json`: source of truth for gallery entries - `data/elo.json`: Elo ratings, record totals, and ranking settings - `scripts/build.js`: renders static pages from templates and data +- `scripts/check.js`: validates data, image assets, and generated pages - `scripts/generate-thumbnails.js`: regenerates thumbnails from the full-size images - `scripts/ingest-meal.js`: ingests a new meal image and metadata in one command +- `scripts/serve.js`: serves the generated site locally with a small static file server - `scripts/lib/elo.js`: validates and syncs Elo data against the meal list - `package.json`: minimal Node build entrypoint +## Run Locally + +Install dependencies: + +```sh +npm install +``` + +Build the site and validate the generated output: + +```sh +npm run build +``` + +Serve it locally: + +```sh +npm run serve +``` + +Then open `http://127.0.0.1:4321`. + +If you want a single command that builds and serves, run: + +```sh +npm start +``` + +To validate the repo state without rebuilding thumbnails or pages, run: + +```sh +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. @@ -99,4 +135,3 @@ The `x` and `y` values are normalized from `0` to `1`, where `0.5, 0.5` is the c ## Planned Features 1. Optional shared sync or export/import for rankings if browser-local persistence becomes too limiting. -2. General cleanup and history cleanup once the bigger structural changes are in place. diff --git a/package.json b/package.json index 843a8a0..b3c2b28 100644 --- a/package.json +++ b/package.json @@ -2,11 +2,14 @@ "name": "gallery", "private": true, "scripts": { - "build": "npm run build:thumbs && npm run build:pages", + "build": "npm run build:thumbs && npm run build:pages && npm run check", "ingest": "node scripts/ingest-meal.js", + "check": "node scripts/check.js", "build:pages": "node scripts/build.js", "build:thumbs": "node scripts/generate-thumbnails.js", - "build:thumbs:force": "node scripts/generate-thumbnails.js --force" + "build:thumbs:force": "node scripts/generate-thumbnails.js --force", + "serve": "node scripts/serve.js", + "start": "npm run build && npm run serve" }, "dependencies": { "sharp": "^0.34.5" diff --git a/scripts/build.js b/scripts/build.js index 7b378de..ba7a69e 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -2,7 +2,7 @@ const fs = require("fs"); const path = require("path"); const { getRankedMeals, syncEloWithMeals } = require("./lib/elo"); -const { loadMeals, repoRoot } = require("./lib/meals"); +const { loadMeals, repoRoot, validateMealAssets } = require("./lib/meals"); const indexTemplatePath = path.join(repoRoot, "templates", "index.html"); const indexOutputPath = path.join(repoRoot, "index.html"); @@ -126,6 +126,7 @@ function replaceBlock(template, token, replacement) { } function buildIndex(meals = loadMeals()) { + validateMealAssets(meals); const template = fs.readFileSync(indexTemplatePath, "utf8"); const eol = detectEol(template); @@ -136,6 +137,7 @@ function buildRankings( meals = loadMeals(), eloData = syncEloWithMeals(meals) ) { + validateMealAssets(meals); const template = fs.readFileSync(rankingsTemplatePath, "utf8"); const eol = detectEol(template); const rankedMeals = getRankedMeals(meals, eloData); diff --git a/scripts/check.js b/scripts/check.js new file mode 100644 index 0000000..cdf916d --- /dev/null +++ b/scripts/check.js @@ -0,0 +1,151 @@ +const fs = require("fs"); +const path = require("path"); + +const { getEloAlignmentReport, loadEloData } = require("./lib/elo"); +const { + fullsDir, + loadMeals, + repoRoot, + thumbsDir, + validateMealAssets, +} = require("./lib/meals"); + +const indexPath = path.join(repoRoot, "index.html"); +const rankingsPath = path.join(repoRoot, "rankings.html"); + +function listJpgIds(directoryPath) { + if (!fs.existsSync(directoryPath)) { + return []; + } + + return fs + .readdirSync(directoryPath, { withFileTypes: true }) + .filter( + (entry) => entry.isFile() && path.extname(entry.name).toLowerCase() === ".jpg" + ) + .map((entry) => path.basename(entry.name, ".jpg")) + .sort((left, right) => left.localeCompare(right, undefined, { numeric: true })); +} + +function getUnexpectedIds(directoryPath, expectedIds) { + return listJpgIds(directoryPath).filter((id) => !expectedIds.has(id)); +} + +function countMatches(text, pattern) { + return (text.match(pattern) || []).length; +} + +function parseRankingsSeedData(rankingsHtml) { + const match = rankingsHtml.match( + /