Initial commit — Bar Pivoine cocktail recipe site

Hugo Extended site with 426 cocktail recipes from the open cocktail dataset.
Dark amber/gold editorial aesthetic, Tailwind CSS v4, Alpine.js client-side
search and filtering, HTMX page transitions, Docker + nginx production build.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 11:53:45 +02:00
commit b3b9fb7ac6
462 changed files with 9012 additions and 0 deletions
+310
View File
@@ -0,0 +1,310 @@
# 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
- `<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
```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.