add: build script that renders index from meals
All checks were successful
Deploy on push / deploy (push) Has been skipped
All checks were successful
Deploy on push / deploy (push) Has been skipped
This commit is contained in:
14
README.md
14
README.md
@@ -6,24 +6,26 @@ The site is based on the HTML5 UP Lens template and currently ships as a plain s
|
|||||||
|
|
||||||
## Repo Layout
|
## Repo Layout
|
||||||
|
|
||||||
- `index.html`: main gallery page
|
- `templates/index.html`: source template for the main gallery page
|
||||||
|
- `index.html`: generated static gallery page
|
||||||
- `assets/`: site CSS, JavaScript, fonts, and audio
|
- `assets/`: site CSS, JavaScript, fonts, and audio
|
||||||
- `images/fulls/`: full-size gallery images
|
- `images/fulls/`: full-size gallery images
|
||||||
- `images/thumbs/`: gallery thumbnails
|
- `images/thumbs/`: gallery thumbnails
|
||||||
- `data/meals.json`: source of truth for gallery entries
|
- `data/meals.json`: source of truth for gallery entries
|
||||||
- `scripts/render-gallery.js`: regenerates the gallery markup in `index.html` from `data/meals.json`
|
- `scripts/build.js`: renders static pages from templates and data
|
||||||
|
- `package.json`: minimal Node build entrypoint
|
||||||
|
|
||||||
## Content Workflow
|
## Content Workflow
|
||||||
|
|
||||||
Gallery entries now live in `data/meals.json`.
|
Gallery entries live in `data/meals.json`, and `index.html` is generated from `templates/index.html`.
|
||||||
|
|
||||||
After editing that file, regenerate the static markup with:
|
After editing content or templates, rebuild the site with:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
node scripts/render-gallery.js
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
The script updates only the generated gallery block inside `index.html`.
|
The build currently renders the main page without changing the existing Lens gallery structure, so the current client-side viewer code continues to work.
|
||||||
|
|
||||||
## Image Conventions
|
## Image Conventions
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,6 @@
|
|||||||
|
|
||||||
<!-- Thumbnail -->
|
<!-- Thumbnail -->
|
||||||
<section id="thumbnails">
|
<section id="thumbnails">
|
||||||
<!-- Generated gallery items: start -->
|
|
||||||
<article>
|
<article>
|
||||||
<a class="thumbnail" href="images/fulls/01.jpg" data-position="left center"><img src="images/thumbs/01.jpg" alt="" /></a>
|
<a class="thumbnail" href="images/fulls/01.jpg" data-position="left center"><img src="images/thumbs/01.jpg" alt="" /></a>
|
||||||
<h2>sf on $10</h2>
|
<h2>sf on $10</h2>
|
||||||
@@ -199,7 +198,6 @@
|
|||||||
<h2>hey tea</h2>
|
<h2>hey tea</h2>
|
||||||
<p>mochi yinje black milk tea ts was actually so buss 10/10 only boba i've ever wanted to get again myself</p>
|
<p>mochi yinje black milk tea ts was actually so buss 10/10 only boba i've ever wanted to get again myself</p>
|
||||||
</article>
|
</article>
|
||||||
<!-- Generated gallery items: end -->
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
|
|||||||
7
package.json
Normal file
7
package.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "gallery",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "node scripts/build.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
90
scripts/build.js
Normal file
90
scripts/build.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const repoRoot = path.resolve(__dirname, "..");
|
||||||
|
const mealsPath = path.join(repoRoot, "data", "meals.json");
|
||||||
|
const indexTemplatePath = path.join(repoRoot, "templates", "index.html");
|
||||||
|
const indexOutputPath = path.join(repoRoot, "index.html");
|
||||||
|
|
||||||
|
function detectEol(text) {
|
||||||
|
return text.includes("\r\n") ? "\r\n" : "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return value
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/"/g, """);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateMeals(meals) {
|
||||||
|
if (!Array.isArray(meals)) {
|
||||||
|
throw new Error("data/meals.json must contain an array");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [index, meal] of meals.entries()) {
|
||||||
|
if (!meal || typeof meal !== "object") {
|
||||||
|
throw new Error(`Meal at index ${index} must be an object`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const field of ["id", "title", "description"]) {
|
||||||
|
if (typeof meal[field] !== "string" || meal[field].length === 0) {
|
||||||
|
throw new Error(`Meal ${index} is missing required string field "${field}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meal.position !== undefined && typeof meal.position !== "string") {
|
||||||
|
throw new Error(`Meal ${index} has a non-string "position" value`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGalleryItem(meal, eol) {
|
||||||
|
const attrs = [`class="thumbnail"`, `href="images/fulls/${meal.id}.jpg"`];
|
||||||
|
|
||||||
|
if (meal.position) {
|
||||||
|
attrs.push(`data-position="${escapeHtml(meal.position)}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
"\t\t\t\t<article>",
|
||||||
|
`\t\t\t\t\t<a ${attrs.join(" ")}><img src="images/thumbs/${meal.id}.jpg" alt="" /></a>`,
|
||||||
|
`\t\t\t\t\t<h2>${escapeHtml(meal.title)}</h2>`,
|
||||||
|
`\t\t\t\t\t<p>${escapeHtml(meal.description)}</p>`,
|
||||||
|
"\t\t\t\t</article>",
|
||||||
|
].join(eol);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGallery(meals, eol) {
|
||||||
|
return meals.map((meal) => renderGalleryItem(meal, eol)).join(eol);
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceBlock(template, token, replacement) {
|
||||||
|
const pattern = new RegExp(`^[\\t ]*\\{\\{${token}\\}\\}$`, "m");
|
||||||
|
|
||||||
|
if (!pattern.test(template)) {
|
||||||
|
throw new Error(`Template is missing required block token "{{${token}}}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return template.replace(pattern, () => replacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildIndex() {
|
||||||
|
const template = fs.readFileSync(indexTemplatePath, "utf8");
|
||||||
|
const eol = detectEol(template);
|
||||||
|
const meals = JSON.parse(fs.readFileSync(mealsPath, "utf8"));
|
||||||
|
|
||||||
|
validateMeals(meals);
|
||||||
|
|
||||||
|
return replaceBlock(template, "gallery_items", renderGallery(meals, eol));
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeFile(filePath, contents) {
|
||||||
|
fs.writeFileSync(filePath, contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
writeFile(indexOutputPath, buildIndex());
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
|
|
||||||
const repoRoot = path.resolve(__dirname, "..");
|
|
||||||
const dataPath = path.join(repoRoot, "data", "meals.json");
|
|
||||||
const indexPath = path.join(repoRoot, "index.html");
|
|
||||||
|
|
||||||
const startMarker = "<!-- Generated gallery items: start -->";
|
|
||||||
const endMarker = "<!-- Generated gallery items: end -->";
|
|
||||||
|
|
||||||
function escapeHtml(value) {
|
|
||||||
return value
|
|
||||||
.replace(/&/g, "&")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/\"/g, """);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderArticle(entry, eol) {
|
|
||||||
const attrs = [`class="thumbnail"`, `href="images/fulls/${entry.id}.jpg"`];
|
|
||||||
|
|
||||||
if (entry.position) {
|
|
||||||
attrs.push(`data-position="${escapeHtml(entry.position)}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
"\t\t\t\t<article>",
|
|
||||||
`\t\t\t\t\t<a ${attrs.join(" ")}><img src="images/thumbs/${entry.id}.jpg" alt="" /></a>`,
|
|
||||||
`\t\t\t\t\t<h2>${escapeHtml(entry.title)}</h2>`,
|
|
||||||
`\t\t\t\t\t<p>${escapeHtml(entry.description)}</p>`,
|
|
||||||
"\t\t\t\t</article>",
|
|
||||||
].join(eol);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderGallery(entries, eol) {
|
|
||||||
return entries.map((entry) => renderArticle(entry, eol)).join(eol);
|
|
||||||
}
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
const entries = JSON.parse(fs.readFileSync(dataPath, "utf8"));
|
|
||||||
const indexHtml = fs.readFileSync(indexPath, "utf8");
|
|
||||||
const eol = indexHtml.includes("\r\n") ? "\r\n" : "\n";
|
|
||||||
const galleryMarkup = renderGallery(entries, eol);
|
|
||||||
const replacement = [
|
|
||||||
`\t\t\t\t${startMarker}`,
|
|
||||||
galleryMarkup,
|
|
||||||
`\t\t\t\t${endMarker}`,
|
|
||||||
].join(eol);
|
|
||||||
|
|
||||||
const markerPattern = new RegExp(
|
|
||||||
`^[\\t ]*${startMarker}[\\s\\S]*?^[\\t ]*${endMarker}`,
|
|
||||||
"m"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!markerPattern.test(indexHtml)) {
|
|
||||||
throw new Error("Could not find gallery markers in index.html");
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(indexPath, indexHtml.replace(markerPattern, replacement));
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
56
templates/index.html
Normal file
56
templates/index.html
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<!DOCTYPE HTML>
|
||||||
|
<!--
|
||||||
|
Lens by HTML5 UP
|
||||||
|
html5up.net | @ajlkn
|
||||||
|
Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
|
||||||
|
-->
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>vicky n pham fanpage</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
||||||
|
<link rel="stylesheet" href="assets/css/main.css" />
|
||||||
|
<link rel="stylesheet" href="assets/css/nyaa.css" />
|
||||||
|
<noscript><link rel="stylesheet" href="assets/css/noscript.css" /></noscript>
|
||||||
|
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="../favicon/apple-touch-icon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="../favicon/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="../favicon/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="../favicon/site.webmanifest">
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body class="is-preload-0 is-preload-1 is-preload-2">
|
||||||
|
|
||||||
|
<!-- Main -->
|
||||||
|
<div id="main">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<header id="header">
|
||||||
|
<img src="images/meow.gif" alt="meow" id="gifone">
|
||||||
|
<h1>for vham :3</h1>
|
||||||
|
<p id="haiku">Please enable javascript >.<</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Thumbnail -->
|
||||||
|
<section id="thumbnails">
|
||||||
|
{{gallery_items}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer id="footer">
|
||||||
|
<ul class="copyright">
|
||||||
|
<li>© Ryan Chou. 2026.</li>
|
||||||
|
</ul>
|
||||||
|
<img src="images/nyaa.gif" alt="nyaa" id="giftwo">
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="assets/js/jquery.min.js"></script>
|
||||||
|
<script src="assets/js/browser.min.js"></script>
|
||||||
|
<script src="assets/js/breakpoints.min.js"></script>
|
||||||
|
<script src="assets/js/haiku.js"></script>
|
||||||
|
<script src="assets/js/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user