2026-06-08 10:56:54 +02:00

Bar Pivoine

A moody, editorial cocktail recipe site. 425 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 (425 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

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, ingredientMeasures
  • drinkThumbnail (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/10rgba(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:

[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:

  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
  • <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 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

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


License

Content is derived from the open cocktail dataset. Site code is © 2026 Bar Pivoine / pivoine.art.

S
Description
A moody, editorial cocktail recipe site. 425 recipes from the open cocktail dataset, served with AI-generated photography, client-side search, and a dark amber aesthetic.
https://bar.pivoine.art
Readme 287 MiB
Languages
HTML 68%
JavaScript 21.2%
CSS 9.5%
Dockerfile 1.3%