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.
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:
-`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 |
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__` |
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