#!/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, "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}`);