Required by --frozen-lockfile in Dockerfile stage 1. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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
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 v0.147+
- Node.js v22+
- pnpm v9+
Development
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
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 (prototype/uploads/final_cocktails.csv).
Generate recipe pages
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,ingredientMeasuresdrinkThumbnail(original dataset URL, used as fallback)- Body: preparation instructions
Generate AI images
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 |
# 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 componentsonly 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:
[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:
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
docker build -t bar-pivoine .
docker run -p 8080:80 bar-pivoine
The image is two stages:
- Builder (
node:22-alpine) — installs Hugo Extended binary, runspnpm installandhugo --minify - Production (
nginx:alpine) — copiespublic/into nginx, appliesnginx.conf
nginx features
- Gzip compression for HTML, CSS, JS, SVG, JSON
Cache-Control: no-storefor HTML;immutable, max-age=31536000for 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
<meta name="description">from page description or site default- Open Graph (
og:title,og:description,og:image,og:type) - Twitter Card (
summary_large_image) - JSON-LD
WebSiteschema on the homepage - JSON-LD
Recipeschema on every cocktail detail page (name, ingredients, instructions, image, category) robots.txtgenerated by Hugo (enableRobotsTXT = true)- XML sitemap at
/sitemap.xml
Code Formatting
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 (Kaggle / aadyasingh55)
- Imagery: AI-generated via FLUX.2 pro (Black Forest Labs / Replicate)
License
Content is derived from the open cocktail dataset. Site code is © 2026 Bar Pivoine / pivoine.art.