Initial commit — Bar Pivoine cocktail recipe site

Hugo Extended site with 426 cocktail recipes from the open cocktail dataset.
Dark amber/gold editorial aesthetic, Tailwind CSS v4, Alpine.js client-side
search and filtering, HTMX page transitions, Docker + nginx production build.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 11:53:45 +02:00
commit b3b9fb7ac6
462 changed files with 9012 additions and 0 deletions
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env node
/**
* generate-content.mjs
* Reads prototype/uploads/final_cocktails.csv and creates Hugo content bundles
* under content/recipes/{slug}/index.md
*
* Usage: node scripts/generate-content.mjs
*/
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import { parse } from "csv-parse/sync";
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, "..");
const CSV_PATH = join(ROOT, "prototype/uploads/final_cocktails.csv");
const CONTENT_DIR = join(ROOT, "content/recipes");
// ── Helpers ──────────────────────────────────────────────────────────────────
function slugify(name) {
return name
.toLowerCase()
.replace(/&/g, "and")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
/** Parse a Python-style list string: "['Gin', 'Grand Marnier', None]" → ["Gin", "Grand Marnier"] */
function parsePythonList(str) {
if (!str || str.trim() === "" || str === "None") return [];
try {
const normalized = str
.replace(/None/g, "null")
.replace(/'/g, '"');
const arr = JSON.parse(normalized);
return arr.filter((v) => v !== null && v !== undefined && String(v).trim() !== "");
} catch {
// Fallback: extract quoted strings manually
const matches = str.match(/'([^']+)'/g);
if (matches) return matches.map((m) => m.replace(/'/g, "").trim()).filter(Boolean);
return [];
}
}
/** Escape a YAML string value, wrapping in double quotes and escaping internals */
function yamlString(val) {
if (!val) return '""';
const escaped = String(val).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
return `"${escaped}"`;
}
/** Build a YAML sequence from a JS array of strings */
function yamlList(arr) {
if (!arr || arr.length === 0) return "[]";
return `[${arr.map((v) => yamlString(v)).join(", ")}]`;
}
/** Generate a SEO description from cocktail fields */
function buildDescription(name, category, glass, ingredients) {
const ingredientSummary =
ingredients.length > 0
? `Made with ${ingredients.slice(0, 4).join(", ")}${ingredients.length > 4 ? " and more" : ""}.`
: "";
return `${name}: a ${category} served in a ${glass}. ${ingredientSummary}`.trim();
}
// ── Main ──────────────────────────────────────────────────────────────────────
const csv = readFileSync(CSV_PATH, "utf-8");
const records = parse(csv, {
columns: true,
skip_empty_lines: true,
relax_column_count: true,
trim: true,
});
console.log(`Parsed ${records.length} cocktails from CSV`);
let created = 0;
let skipped = 0;
const slugMap = new Map(); // track duplicates
for (const row of records) {
const name = (row.name || "").trim();
if (!name) continue;
let slug = slugify(name);
// Handle duplicate slugs by appending the id
if (slugMap.has(slug)) {
slug = `${slug}-${row.id || slugMap.size}`;
}
slugMap.set(slug, true);
const dir = join(CONTENT_DIR, slug);
const file = join(dir, "index.md");
if (existsSync(file)) {
skipped++;
continue;
}
const ingredients = parsePythonList(row.ingredients);
const measures = parsePythonList(row.ingredientMeasures);
const category = (row.category || "Cocktail").trim();
const glass = (row.glassType || "Cocktail glass").trim();
const alcoholic = (row.alcoholic || "").trim();
const thumbnail = (row.drinkThumbnail || "").trim();
const instructions = (row.instructions || "").trim();
const description = buildDescription(name, category, glass, ingredients);
mkdirSync(dir, { recursive: true });
const frontmatter = [
"---",
`title: ${yamlString(name)}`,
`date: "2026-01-01"`,
`draft: false`,
`description: ${yamlString(description)}`,
`alcoholic: ${yamlString(alcoholic)}`,
`categories: ${yamlList([category])}`,
`glasses: ${yamlList([glass])}`,
`ingredients: ${yamlList(ingredients)}`,
`ingredientMeasures: ${yamlList(measures)}`,
`drinkThumbnail: ${yamlString(thumbnail)}`,
"---",
"",
instructions,
"",
].join("\n");
writeFileSync(file, frontmatter, "utf-8");
created++;
if (created % 50 === 0) {
process.stdout.write(` [${created}/${records.length}] created…\r`);
}
}
console.log(`\nDone. Created: ${created}, Skipped (already exist): ${skipped}`);