5ca68c1b04
- Point both generate scripts to final_cocktails.csv at repo root instead of prototype/uploads/final_cocktails.csv - Add final_cocktails.csv to repo - Switch flux-2-pro to input_images[], resolution "2 MP", output_quality 100 - Lower safety_tolerance from 6 → 2 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
142 lines
4.5 KiB
JavaScript
142 lines
4.5 KiB
JavaScript
#!/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}`);
|