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
+58
View File
@@ -0,0 +1,58 @@
{{/* Usage: {{ partial "cocktail-card.html" . }} — pass a cocktail Page */}}
{{- $cocktail := . -}}
{{- $img := $cocktail.Resources.GetMatch "cocktail.*" -}}
{{- $nob := "Optional" -}}
{{- with $cocktail.Params.alcoholic -}}
{{- if eq . "Alcoholic" -}}{{- $nob = "Spirited" -}}
{{- else if eq . "Non alcoholic" -}}{{- $nob = "Zero-proof" -}}
{{- end -}}
{{- end -}}
<a
href="{{ $cocktail.RelPermalink }}"
class="block border border-warm/10 rounded-[14px] overflow-hidden bg-surface transition-[transform,border-color,box-shadow] duration-[260ms] ease-[cubic-bezier(.2,.7,.3,1)] relative hover:-translate-y-1 hover:border-warm/18 hover:shadow-[0_24px_50px_-24px_rgba(0,0,0,.8)] card-wrap"
aria-label="{{ $cocktail.Title }}"
>
<!-- Thumbnail -->
<div class="aspect-square relative overflow-hidden">
{{- if $img -}}
{{- partial "img.html" (dict
"res" $img
"widths" (slice 500 900)
"sizes" "(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
"class" "card-img"
"alt" $cocktail.Title
) -}}
{{- else -}}
<img
src="{{ $cocktail.Params.drinkThumbnail }}"
alt="{{ $cocktail.Title }}"
loading="lazy"
decoding="async"
class="card-img"
/>
{{- end -}}
<!-- Nob badge -->
<span class="absolute top-[11px] left-[11px] z-[2] font-mono text-[9px] tracking-[0.12em] uppercase px-[9px] py-1 rounded-full bg-[rgba(13,10,7,0.6)] backdrop-blur-[6px] border border-warm/10 text-ink-soft">
{{ $nob }}
</span>
</div>
<!-- Body -->
<div class="px-[17px] pt-4 pb-[18px]">
<div class="font-mono text-[10px] tracking-[0.16em] uppercase text-gold">
{{- with $cocktail.Params.categories -}}{{ index . 0 }}{{- end -}}
</div>
<h3 class="font-serif font-semibold text-[23px] leading-[1.05] tracking-tight mt-[7px] mb-[10px]">
{{ $cocktail.Title }}
</h3>
<div class="flex flex-wrap gap-1.5 font-mono text-[10px] text-ink-mute tracking-[0.04em]">
{{- with $cocktail.Params.glasses -}}
<span>{{ index . 0 }}</span>
{{- end -}}
{{- with $cocktail.Params.ingredients -}}
<span class="before:content-['·'] before:mr-1.5 before:opacity-60">{{ len . }} ingredients</span>
{{- end -}}
</div>
</div>
</a>
+57
View File
@@ -0,0 +1,57 @@
<footer class="border-t border-warm/10 bg-bg-deep pt-16 pb-10 mt-10 relative">
<div class="max-w-[1280px] mx-auto px-8 max-[860px]:px-5">
<div class="grid grid-cols-[1.6fr_1fr_1fr_1fr] gap-10 max-[860px]:grid-cols-2 max-[860px]:gap-8">
<!-- Brand column -->
<div>
<a href="/" class="flex flex-col items-start gap-4 no-underline mb-5" aria-label="{{ .Site.Title }}">
{{- partial "mark.html" (dict "size" 58) -}}
<span class="font-serif text-[32px] font-medium tracking-[-0.01em] leading-none text-ink">Bar Pivoine</span>
</a>
<div class="w-full h-px bg-warm/10 mb-4"></div>
{{- $count := len .Site.RegularPages -}}
<p class="text-ink-mute text-sm leading-relaxed">A field guide to the good pour. {{ $count }} cocktail recipes, low light, honest measures, no garnish left behind.</p>
<p class="font-mono text-[11px] text-ink-faint leading-relaxed mt-3">Recipes from the <a href="https://www.kaggle.com/datasets/aadyasingh55/cocktails" class="underline underline-offset-2 hover:text-ink-soft transition-colors duration-[180ms]">open cocktail dataset</a> · imagery via FLUX.2 pro</p>
</div>
<!-- Categories -->
<div>
<h5 class="font-mono text-[11px] tracking-[0.18em] uppercase text-gold mb-4">Categories</h5>
{{- range first 6 (.Site.Taxonomies.categories.ByCount) -}}
<a href="{{ .Page.RelPermalink }}" class="block text-ink-mute text-[14px] py-[5px] hover:text-ink transition-colors duration-[180ms]">
{{ .Page.Title }}
</a>
{{- end -}}
</div>
<!-- Glassware -->
<div>
<h5 class="font-mono text-[11px] tracking-[0.18em] uppercase text-gold mb-4">Glassware</h5>
{{- range first 6 (.Site.Taxonomies.glasses.ByCount) -}}
<a href="{{ .Page.RelPermalink }}" class="block text-ink-mute text-[14px] py-[5px] hover:text-ink transition-colors duration-[180ms]">
{{ .Page.Title }}
</a>
{{- end -}}
</div>
<!-- Ingredients -->
<div>
<h5 class="font-mono text-[11px] tracking-[0.18em] uppercase text-gold mb-4">Ingredients</h5>
{{- range first 6 (.Site.Taxonomies.ingredients.ByCount) -}}
<a href="{{ .Page.RelPermalink }}" class="block text-ink-mute text-[14px] py-[5px] hover:text-ink transition-colors duration-[180ms]">
{{ .Page.Title }}
</a>
{{- end -}}
</div>
</div>
<!-- Bottom bar -->
<div class="flex justify-between items-center mt-[54px] pt-6 border-t border-warm/10">
<span class="font-mono text-[11px] text-ink-faint tracking-[0.04em]">© 2026 Bar Pivoine</span>
<span class="font-mono text-[11px] text-ink-faint tracking-[0.04em]">Powered by <a href="https://pivoine.art" class="hover:text-ink transition-colors duration-[180ms]">pivoine.art</a></span>
</div>
</div>
</footer>
+107
View File
@@ -0,0 +1,107 @@
<meta charset="utf-8" />
<meta name="htmx-config" content='{"globalViewTransitions":true,"scrollBehavior":"smooth"}' />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#14100c" />
<title>
{{- if .IsHome -}}
{{- .Site.Title }} — {{ .Site.Params.tagline -}}
{{- else -}}
{{- .Title }} — {{ .Site.Title -}}
{{- end -}}
</title>
<meta name="description" content="{{ with .Description }}{{ . }}{{ else }}{{ .Site.Params.description }}{{ end }}" />
<meta name="author" content="{{ .Site.Params.author }}" />
<meta name="robots" content="index, follow" />
<!-- Open Graph -->
<meta property="og:site_name" content="{{ .Site.Title }}" />
<meta
property="og:title"
content="{{ if .IsHome }}{{ .Site.Title }} — {{ .Site.Params.tagline }}{{ else }}{{ .Title }}{{ end }}"
/>
<meta
property="og:description"
content="{{ with .Description }}{{ . }}{{ else }}{{ .Site.Params.description }}{{ end }}"
/>
<meta property="og:url" content="{{ .Permalink }}" />
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}" />
<meta property="og:locale" content="en_US" />
{{- $ogImage := .Site.Params.ogImage | absURL -}}
{{- $ogImageAlt := .Site.Title -}}
{{- with .Resources.GetMatch "cocktail.*" -}}
{{- $resized := .Resize "1200x webp" -}}
{{- $ogImage = $resized.Permalink | absURL -}}
{{- $ogImageAlt = $.Title -}}
{{- else -}}
{{- with .Params.drinkThumbnail -}}
{{- $ogImage = . -}}
{{- $ogImageAlt = $.Title -}}
{{- end -}}
{{- end -}}
<meta property="og:image" content="{{ $ogImage }}" />
<meta property="og:image:alt" content="{{ $ogImageAlt }}" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
{{- if .IsPage }}
<meta property="article:published_time" content="{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}" />
<meta property="article:modified_time" content="{{ .Lastmod.Format "2006-01-02T15:04:05Z07:00" }}" />
{{- range .Params.categories }}<meta property="article:section" content="{{ . }}" />{{ end }}
{{- range .Params.ingredients }}<meta property="article:tag" content="{{ . }}" />{{ end }}
{{- end }}
<!-- Twitter / X Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="{{ if .IsHome }}{{ .Site.Title }} — {{ .Site.Params.tagline }}{{ else }}{{ .Title }}{{ end }}" />
<meta
name="twitter:description"
content="{{ with .Description }}{{ . }}{{ else }}{{ .Site.Params.description }}{{ end }}"
/>
<meta name="twitter:image" content="{{ $ogImage }}" />
<meta name="twitter:image:alt" content="{{ $ogImageAlt }}" />
<!-- Canonical -->
<link rel="canonical" href="{{ .Permalink }}" />
<!-- JSON-LD structured data -->
{{- partial "schema.html" . -}}
<!-- Fonts — non-render-blocking -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="preload"
as="style"
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400;1,500&family=Hanken+Grotesk:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap"
/>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400;1,500&family=Hanken+Grotesk:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap"
media="print"
onload="this.media='all'"
/>
<noscript>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400;1,500&family=Hanken+Grotesk:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap"
/>
</noscript>
<!-- CSS via Hugo Pipes + PostCSS + Tailwind v4 -->
{{- $css := resources.Get "css/main.css" | css.PostCSS -}}
{{- if eq hugo.Environment "production" -}}
{{- $css = $css | minify | fingerprint "sha256" -}}
{{- end -}}
<link
rel="stylesheet"
href="{{ $css.RelPermalink }}"
{{ if eq hugo.Environment "production" }}integrity="{{ $css.Data.Integrity }}"{{ end }}
/>
<!-- Favicon & PWA -->
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="manifest" href="/site.webmanifest" />
+19
View File
@@ -0,0 +1,19 @@
{{/* Usage: {{ partial "icon.html" "coupe" }} */}}
{{- $name := . -}}
{{- if eq $name "arrow-right" -}}
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 8h12M10 4l4 4-4 4"/></svg>
{{- else if eq $name "arrow-left" -}}
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 8H2M6 4L2 8l4 4"/></svg>
{{- else if eq $name "chevron-down" -}}
<svg width="11" height="11" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 4l4 4 4-4"/></svg>
{{- else if eq $name "x" -}}
<svg width="14" height="14" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M1 1l10 10M11 1L1 11"/></svg>
{{- else if eq $name "search" -}}
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--color-gold)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7.5"/><path d="m20.5 20.5-4.8-4.8"/></svg>
{{- else if eq $name "coupe" -}}
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 2h10L9 9H7L3 2z"/><path d="M8 9v4"/><path d="M5.5 13h5"/></svg>
{{- else if eq $name "menu" -}}
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
{{- else if eq $name "glass" -}}
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 22h8"/><path d="M12 11v11"/><path d="M5 3l2 9a5 5 0 0 0 10 0l2-9Z"/></svg>
{{- end -}}
+26
View File
@@ -0,0 +1,26 @@
{{/*
Usage:
{{ partial "img.html" (dict "res" $imgResource "widths" (slice 600 1200) "sizes" "100vw" "class" "w-full h-full object-cover" "alt" "cocktail name" "loading" "lazy") }}
*/}}
{{- $res := .res -}}
{{- $widths := .widths | default (slice 800 1200) -}}
{{- $sizes := .sizes | default "100vw" -}}
{{- $class := .class | default "" -}}
{{- $alt := .alt | default "" -}}
{{- $loading := .loading | default "lazy" -}}
{{- $entries := slice -}}
{{- range $widths -}}
{{- $img := $res.Resize (printf "%dx webp" .) -}}
{{- $entries = $entries | append (printf "%s %dw" $img.RelPermalink .) -}}
{{- end -}}
<img
srcset="{{ delimit $entries ", " }}"
sizes="{{ $sizes }}"
src="{{ ($res.Resize (printf "%dx webp" (index $widths 0))).RelPermalink }}"
alt="{{ $alt }}"
loading="{{ $loading }}"
decoding="async"
class="{{ $class }}"
/>
+17
View File
@@ -0,0 +1,17 @@
{{/* PeonyMark wreath — Usage: {{ partial "mark.html" (dict "size" 34) }} */}}
{{- $size := .size | default 34 -}}
<svg width="{{ $size }}" height="{{ $size }}" viewBox="0 0 100 100" aria-hidden="true" style="flex:none">
{{/* Outer ring — 12 narrow ovals, cy=20 (50-30), opacity 0.44 */}}
{{- range seq 12 -}}
{{- $angle := mul (sub . 1) 30 -}}
<ellipse cx="50" cy="20" rx="4" ry="10" fill="var(--color-gold)" fill-opacity="0.44" transform="rotate({{ $angle }} 50 50)"/>
{{- end -}}
{{/* Inner ring — 8 fuller ovals, cy=32 (50-18), opacity 0.84 */}}
{{- range seq 8 -}}
{{- $angle := mul (sub . 1) 45 -}}
<ellipse cx="50" cy="32" rx="3.5" ry="8" fill="var(--color-gold)" fill-opacity="0.84" transform="rotate({{ $angle }} 50 50)"/>
{{- end -}}
{{/* Centre: dark hole + gold dot */}}
<circle cx="50" cy="50" r="8" fill="var(--color-bg)"/>
<circle cx="50" cy="50" r="4.8" fill="var(--color-gold)"/>
</svg>
+25
View File
@@ -0,0 +1,25 @@
{{- $onCellar := hasPrefix .RelPermalink "/recipes/" -}}
<header class="sticky top-0 z-[60] backdrop-blur-[14px] border-b border-warm/10 bg-[linear-gradient(to_bottom,rgba(13,10,7,0.92),rgba(13,10,7,0.66))]">
<div class="max-w-[1280px] mx-auto px-8 max-[860px]:px-5 flex items-center gap-7 h-[72px]">
<!-- Brand -->
<a href="/" class="flex items-center gap-[13px] shrink-0" aria-label="{{ .Site.Title }}">
{{- partial "mark.html" (dict "size" 34) -}}
<div class="flex flex-col leading-none whitespace-nowrap">
<b class="font-serif font-semibold text-[21px] tracking-[0.02em] text-ink">Bar Pivoine</b>
<span class="font-mono text-[8.5px] tracking-[0.42em] uppercase text-gold mt-[3px]">bar.pivoine.art</span>
</div>
</a>
<!-- Cellar button -->
<a
href="/recipes/"
class="cellar-btn{{ if $onCellar }} active{{ end }}"
aria-current="{{ if $onCellar }}page{{ end }}"
>
{{- partial "icon.html" "coupe" -}}
Cellar
</a>
</div>
</header>
+37
View File
@@ -0,0 +1,37 @@
{{- with .Paginator -}}
<nav class="flex items-center justify-center gap-4 mt-8" aria-label="Pagination">
{{- if .HasPrev -}}
<a href="{{ .Prev.URL }}" class="btn btn-ghost flex items-center gap-2">
{{- partial "icon.html" "arrow-left" -}}
Previous
</a>
{{- else -}}
<span class="btn btn-ghost opacity-30 cursor-not-allowed flex items-center gap-2">
{{- partial "icon.html" "arrow-left" -}}
Previous
</span>
{{- end -}}
<div class="flex items-center gap-2">
{{- range .Pagers -}}
<a
href="{{ .URL }}"
class="{{ if eq . $.Paginator }}w-9 h-9 rounded-full bg-gold text-[#0d0a07] font-semibold font-mono text-sm flex items-center justify-center{{ else }}w-9 h-9 rounded-full border border-warm/10 text-ink-mute hover:border-gold hover:text-gold font-mono text-sm flex items-center justify-center transition-colors{{ end }}"
aria-current="{{ if eq . $.Paginator }}page{{ end }}"
>{{ .PageNumber }}</a>
{{- end -}}
</div>
{{- if .HasNext -}}
<a href="{{ .Next.URL }}" class="btn btn-ghost flex items-center gap-2">
Next
{{- partial "icon.html" "arrow-right" -}}
</a>
{{- else -}}
<span class="btn btn-ghost opacity-30 cursor-not-allowed flex items-center gap-2">
Next
{{- partial "icon.html" "arrow-right" -}}
</span>
{{- end -}}
</nav>
{{- end -}}
+42
View File
@@ -0,0 +1,42 @@
{{- if .IsHome -}}
{{- $schema := dict
"@context" "https://schema.org"
"@type" "WebSite"
"name" .Site.Title
"url" .Site.BaseURL
"description" .Site.Params.description
"publisher" (dict "@type" "Organization" "name" .Site.Title "url" .Site.BaseURL)
"potentialAction" (dict
"@type" "SearchAction"
"target" (dict "@type" "EntryPoint" "urlTemplate" (print .Site.BaseURL "recipes/?q={search_term_string}"))
"query-input" "required name=search_term_string"
)
-}}
{{- printf "<script type=\"application/ld+json\">%s</script>" ($schema | jsonify) | safeHTML -}}
{{- else if and .IsPage (eq .Section "recipes") -}}
{{- $img := "" -}}
{{- with .Resources.GetMatch "cocktail.*" -}}
{{- $img = (.Resize "800x webp").Permalink | absURL -}}
{{- else -}}
{{- $img = .Params.drinkThumbnail | default (.Site.Params.ogImage | absURL) -}}
{{- end -}}
{{- $category := "" -}}
{{- with .Params.categories -}}{{- $category = index . 0 -}}{{- end -}}
{{- $description := .Description | default .Site.Params.description -}}
{{- $schema := dict
"@context" "https://schema.org"
"@type" "Recipe"
"name" .Title
"description" $description
"url" .Permalink
"image" $img
"author" (dict "@type" "Organization" "name" .Site.Title)
"recipeCategory" $category
"recipeIngredient" (.Params.ingredients | default (slice))
"recipeInstructions" (slice (dict "@type" "HowToStep" "text" (.Content | plainify | strings.TrimSpace)))
-}}
{{- printf "<script type=\"application/ld+json\">%s</script>" ($schema | jsonify) | safeHTML -}}
{{- end -}}