5023752f58
README had 426 (off by one due to _index.md). Homepage and footer now use where .Site.RegularPages "Section" "recipes" to exclude non-recipe pages like /imprint/ from the count. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
311 lines
11 KiB
Markdown
311 lines
11 KiB
Markdown
# 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](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 (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
|
||
|
||
- [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.
|