# Bar Pivoine A moody, editorial cocktail recipe site. 426 recipes from the open cocktail dataset, served with AI-generated photography, client-side search, and a dark amber aesthetic. Live at **[bar.pivoine.art](https://bar.pivoine.art)** --- ## Stack | Layer | Technology | |---|---| | Site generator | Hugo Extended v0.154.3 | | CSS | Tailwind CSS v4 via `@tailwindcss/postcss` | | Interactivity | Alpine.js v3.14.8 (CDN) + HTMX v2.0.4 (CDN) | | Page transitions | HTMX `hx-boost` + View Transitions API | | Analytics | Umami (self-hosted at `umami.pivoine.art`) | | Images | Hugo image processor (WebP srcset) + Replicate FLUX.2 pro | | Package manager | pnpm | | Container | Docker multi-stage → nginx:alpine | --- ## Project Structure ``` bar/ ├── hugo.toml # Site config, taxonomies, params ├── package.json # pnpm scripts, Tailwind v4, Prettier ├── postcss.config.js # @tailwindcss/postcss + autoprefixer ├── Dockerfile # Multi-stage: node+hugo builder → nginx ├── nginx.conf # Gzip, cache headers, clean URLs, security headers │ ├── assets/ │ ├── css/main.css # Tailwind v4 @theme tokens + @layer components │ └── js/main.js # Alpine cocktailSearch(), HTMX progress bar │ ├── content/ │ ├── _index.md # Homepage front matter │ ├── recipes/ │ │ ├── _index.md # Archive front matter │ │ └── {slug}/ │ │ ├── index.md # Cocktail page (426 bundles) │ │ └── cocktail.webp # AI-generated image (optional) │ ├── layouts/ │ ├── _default/ │ │ ├── baseof.html # Base shell: HTMX, Alpine, Umami, progress bar │ │ ├── list.html # Generic list fallback │ │ ├── single.html # Generic single fallback │ │ ├── terms.html # Taxonomy overview (e.g. /categories/) │ │ └── term.html # Single taxonomy term (e.g. /categories/cocktail/) │ ├── partials/ │ │ ├── head.html # SEO meta, OG, Twitter card, JSON-LD, fonts, CSS │ │ ├── schema.html # JSON-LD: WebSite (home) + Recipe (detail pages) │ │ ├── nav.html # Sticky header with cellar button │ │ ├── footer.html # Taxonomy columns + brand + attribution │ │ ├── mark.html # PeonyMark wreath SVG partial │ │ ├── icon.html # SVG icon partial (search, arrows, chevron, x) │ │ ├── img.html # Hugo WebP srcset generator │ │ ├── cocktail-card.html # Reusable grid card │ │ ├── pagination.html # Hugo paginator (prev/next + numbered pages) │ │ └── schema.html # Structured data │ ├── index.html # Homepage: hero, explore index, selected pours │ ├── recipes/ │ │ ├── list.html # Archive: Alpine search + filter + pagination │ │ └── single.html # Recipe detail: image, ingredients, method, related │ └── 404.html │ ├── static/ │ ├── favicon.svg # PeonyMark wreath on dark rounded square │ ├── images/ │ │ └── og-default.jpg # Default OG share image (1200×630) │ └── site.webmanifest │ └── scripts/ ├── generate-content.mjs # CSV → Hugo content bundles └── generate-images.mjs # Replicate FLUX.2 pro → cocktail.webp per recipe ``` --- ## Getting Started ### Prerequisites - [Hugo Extended](https://gohugo.io/installation/) v0.147+ - [Node.js](https://nodejs.org/) v22+ - [pnpm](https://pnpm.io/) v9+ ### Development ```bash pnpm install pnpm dev # hugo server --buildDrafts --disableFastRender ``` The dev server starts at `http://localhost:1313` with live reload. Tailwind CSS is processed via Hugo Pipes on every change. ### Production build ```bash pnpm build # NODE_ENV=production hugo --minify ``` Output goes to `public/`. CSS is fingerprinted and integrity-hashed. --- ## Content Generation Cocktail content is generated from the [open cocktail dataset](https://www.kaggle.com/datasets/aadyasingh55/cocktails) (`prototype/uploads/final_cocktails.csv`). ### Generate recipe pages ```bash pnpm generate:content ``` Reads the CSV and creates `content/recipes/{slug}/index.md` for every cocktail. Already-existing files are skipped (idempotent). Each page bundle includes: - `title`, `date`, `description` (auto-generated) - `alcoholic`, `categories`, `glasses`, `ingredients`, `ingredientMeasures` - `drinkThumbnail` (original dataset URL, used as fallback) - Body: preparation instructions ### Generate AI images ```bash REPLICATE_API_TOKEN=r8_... pnpm generate:images ``` Calls Replicate's `black-forest-labs/flux-2-pro` model for each cocktail that doesn't yet have a `cocktail.webp`, using the dataset's reference photo as the image prompt input. Output is saved as a Hugo page resource alongside the content file so Hugo's image processor can generate WebP srcsets. **Options:** | Flag | Description | |---|---| | `--limit N` | Process only the first N cocktails | | `--slug NAME` | Process a single cocktail by slug | | `--concurrency N` | Parallel API requests (default: 3) | | `--dry-run` | Log what would run without calling the API | ```bash # Generate images for 10 cocktails to test REPLICATE_API_TOKEN=r8_... node scripts/generate-images.mjs --limit 10 # Regenerate a single recipe REPLICATE_API_TOKEN=r8_... node scripts/generate-images.mjs --slug margarita ``` --- ## Design System ### Color tokens (`@theme` in `assets/css/main.css`) | Token | Value | Usage | |---|---|---| | `--color-bg` | `#14100c` | Page background | | `--color-bg-deep` | `#0d0a07` | Nav, footer | | `--color-surface` | `#1c1611` | Cards, inputs | | `--color-surface-2` | `#241c15` | Hover surfaces | | `--color-warm` | `#e9d2b4` | Border opacity base (`border-warm/10`, `border-warm/18`) | | `--color-ink` | `#efe6da` | Primary text | | `--color-ink-soft` | `#c9bbab` | Secondary text | | `--color-ink-mute` | `#8d8073` | Muted text | | `--color-ink-faint` | `#5f574d` | Faint labels | | `--color-gold` | `#cf9648` | Primary accent | | `--color-gold-2` | `#e3ad5e` | Hover accent | | `--color-gold-deep` | `#9c6a2c` | Deep accent | ### Typography | Token | Font stack | |---|---| | `--font-serif` | Cormorant Garamond, Georgia, serif | | `--font-sans` | Hanken Grotesk, system-ui, sans-serif | | `--font-mono` | JetBrains Mono, ui-monospace, monospace | All three are loaded from Google Fonts in `layouts/partials/head.html` using `display=swap`. ### Tailwind v4 conventions - Border opacity via modifier: `border-warm/10` → `rgba(233,210,180,0.10)` - Custom arbitrary values inline where one-off: `text-[clamp(44px,6.4vw,80px)]` - `@layer components` only for styles requiring descendant selectors or pseudo-elements (`.card-wrap:hover .card-img`, `.frame-overlay::after`, `.arch-input::placeholder`) - Everything else is Tailwind utility classes directly in HTML --- ## Taxonomies Hugo taxonomies are defined in `hugo.toml`: ```toml [taxonomies] category = "categories" glass = "glasses" ingredient = "ingredients" ``` The `alcoholic` field (Alcoholic / Non alcoholic / Optional alcohol) is **not** a Hugo taxonomy — it is a plain front matter param. The archive page filters by it client-side via Alpine, linking to `/recipes/?alcoholic={slug}`. --- ## Client-side Search (`assets/js/main.js`) The `/recipes/` archive embeds the full cocktail index as `window.__COCKTAILS__` (built at Hugo build time via `{{ $data | jsonify | safeJS }}`), then hands it to an Alpine.js component: ### `cocktailSearch()` component | Property | Description | |---|---| | `q` | Free-text search query | | `active` | Object of active filter slugs: `{ alcoholic, category, glass, ingredient }` | | `page` | Current pagination page (1-indexed) | | `perPage` | Cards per page (24) | | `openFilter` | Which dropdown is open (`null` or key string) | | `all` | Full cocktail array from `window.__COCKTAILS__` | | `tax` | Computed facets `{ alcoholic[], category[], glass[], ingredient[] }` | ### `filtered` getter Chains four slug-exact filters + a free-text substring match across name, category, glass, and all ingredient names. ### `buildTax(list)` Computes taxonomy facets from the cocktail array — groups by value, counts occurrences, sorts by count descending. Produces the dropdown option lists. ### `taxSlug(s)` Slugifies taxonomy values identically to Hugo's URL generation: ```js String(s).toLowerCase() .replace(/&/g, "and") .replace(/[^a-z0-9]+/g, "-") .replace(/^-|-$/g, "") ``` ### URL state `pushState()` encodes active filters and current page into the URL query string (`?q=...&category=...&page=...`) so the search state is bookmarkable and shareable. --- ## Page Transitions HTMX `hx-boost` intercepts all internal link clicks and replaces `#main-content` via XHR. The View Transitions API is wired in `assets/css/main.css` with `@keyframes page-in/page-out` for a crossfade. A CSS progress bar (`#progress-bar`) animates on `htmx:beforeRequest` / `htmx:afterSwap`. --- ## Docker ### Build and run ```bash docker build -t bar-pivoine . docker run -p 8080:80 bar-pivoine ``` The image is two stages: 1. **Builder** (`node:22-alpine`) — installs Hugo Extended binary, runs `pnpm install` and `hugo --minify` 2. **Production** (`nginx:alpine`) — copies `public/` into nginx, applies `nginx.conf` ### nginx features - Gzip compression for HTML, CSS, JS, SVG, JSON - `Cache-Control: no-store` for HTML; `immutable, max-age=31536000` for fingerprinted assets - Security headers: `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy` - Clean URLs via `try_files $uri $uri/ $uri.html` - Custom 404 page --- ## SEO - Canonical URLs on every page - `` from page description or site default - Open Graph (`og:title`, `og:description`, `og:image`, `og:type`) - Twitter Card (`summary_large_image`) - JSON-LD `WebSite` schema on the homepage - JSON-LD `Recipe` schema on every cocktail detail page (name, ingredients, instructions, image, category) - `robots.txt` generated by Hugo (`enableRobotsTXT = true`) - XML sitemap at `/sitemap.xml` --- ## Code Formatting ```bash pnpm format # Prettier write pnpm format:check # Prettier check (CI) ``` Prettier is configured with `prettier-plugin-go-template` (Hugo HTML templates) and `prettier-plugin-toml`. --- ## Data Attribution - Recipes: [Open Cocktail Dataset](https://www.kaggle.com/datasets/aadyasingh55/cocktails) (Kaggle / aadyasingh55) - Imagery: AI-generated via [FLUX.2 pro](https://replicate.com/black-forest-labs/flux-2-pro) (Black Forest Labs / Replicate) --- ## License Content is derived from the open cocktail dataset. Site code is © 2026 Bar Pivoine / pivoine.art.