brewlog/templates/pages/brew.html
Jon Seager 9e5dff7f5f
feat(web): add Open Graph and Twitter Card meta tags
- Add og:title, og:description, og:image, og:type, og:site_name and
  twitter:card meta tags to base template with Askama block overrides
- Create static 1200x630 OG image served at /og-image.png
- Override OG blocks with entity-specific content on brew, cup, and
  stats detail pages
- Reuse BREWLOG_RP_ORIGIN as base URL for absolute og:image URLs via
  OnceLock initialized at server startup
2026-02-08 16:19:34 +00:00

195 lines
7.3 KiB
HTML

{% extends "base.html" %}
{% import "partials/icons.html" as icons %}
{% block title %}Brewlog · {{ brew.roast_name }}{% endblock %}
{% block description %}{{ brew.roast_name }} by {{ brew.roaster_name }} — {{ brew.coffee_weight }} coffee, {{ brew.water_volume }} water.{% endblock %}
{% block og_title %}{{ brew.roast_name }} — Brewlog{% endblock %}
{% block og_description %}{{ brew.roast_name }} by {{ brew.roaster_name }} — {{ brew.coffee_weight }} coffee, {{ brew.water_volume }} water.{% endblock %}
{% block head %}<meta property="og:image" content="{{ base_url }}/og-image.png" />{% endblock %}
{% block content %}
<header class="flex items-start justify-between gap-4">
<div class="flex flex-col gap-2">
<h1 class="text-3xl font-semibold">{{ brew.roast_name }}</h1>
<p class="text-sm text-text-secondary">
{{ brew.roaster_name }} · Brewed {{ brew.created_date }} at {{ brew.created_time }}
</p>
</div>
<button onclick="shareLink(this)" class="inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm font-medium text-text-muted transition hover:bg-surface-alt hover:text-text shrink-0">
<span class="share-icon">{{ icons::clipboard("h-4 w-4 shrink-0") }}</span>
<span class="share-icon-check hidden">{{ icons::check("h-4 w-4 shrink-0") }}</span>
<span class="share-label">Share</span>
</button>
</header>
<script>
const shareLink = (btn) => {
navigator.clipboard.writeText(window.location.href).then(() => {
btn.querySelector('.share-icon').classList.add('hidden');
btn.querySelector('.share-icon-check').classList.remove('hidden');
btn.querySelector('.share-label').textContent = 'Copied';
setTimeout(() => {
btn.querySelector('.share-icon').classList.remove('hidden');
btn.querySelector('.share-icon-check').classList.add('hidden');
btn.querySelector('.share-label').textContent = 'Share';
}, 2000);
});
};
</script>
{# ── Coffee + map ── #}
<div class="grid gap-6 md:grid-cols-2">
<div class="rounded-lg border bg-surface p-5">
<h2 class="text-lg font-semibold text-text mb-4">Coffee</h2>
<dl class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
<div>
<dt class="text-text-muted">Roast</dt>
<dd class="font-medium text-text">{{ brew.roast_name }}</dd>
</div>
<div>
<dt class="text-text-muted">Roaster</dt>
<dd class="font-medium text-text">{{ brew.roaster_name }}</dd>
</div>
{% if brew.origin != "\u{2014}" %}
<div>
<dt class="text-text-muted">Origin</dt>
<dd class="font-medium text-text">
{% if !brew.origin_flag.is_empty() %}{{ brew.origin_flag }} {% endif %}{{ brew.origin }}
</dd>
</div>
{% endif %}
{% if brew.region != "\u{2014}" %}
<div>
<dt class="text-text-muted">Region</dt>
<dd class="font-medium text-text">{{ brew.region }}</dd>
</div>
{% endif %}
{% if brew.producer != "\u{2014}" %}
<div>
<dt class="text-text-muted">Producer</dt>
<dd class="font-medium text-text">{{ brew.producer }}</dd>
</div>
{% endif %}
{% if brew.process != "\u{2014}" %}
<div>
<dt class="text-text-muted">Process</dt>
<dd class="font-medium text-text">{{ brew.process }}</dd>
</div>
{% endif %}
</dl>
{% if !brew.tasting_notes.is_empty() %}
<div class="mt-4 flex flex-wrap gap-1.5">
{% for note in brew.tasting_notes %}
<span class="{{ note.pill_class }}">{{ note.label }}</span>
{% endfor %}
</div>
{% endif %}
</div>
{% if !brew.map_countries.is_empty() %}
<div class="relative rounded-lg border bg-surface overflow-hidden flex items-center">
<world-map
class="block w-full"
data-countries="{{ brew.map_countries }}"
data-max="{{ brew.map_max }}"
></world-map>
<div class="absolute top-2 right-2 flex flex-col gap-1 text-2xs text-text-muted">
<span class="inline-flex items-center gap-1">
<span class="inline-block h-2.5 w-2.5 rounded-sm bg-accent"></span> Origin
</span>
<span class="inline-flex items-center gap-1">
<span class="inline-block h-2.5 w-2.5 rounded-sm bg-accent opacity-50"></span> Roaster
</span>
</div>
</div>
{% endif %}
</div>
{# ── Roaster & gear ── #}
<div class="grid gap-6 md:grid-cols-2">
<div class="rounded-lg border bg-surface p-5">
<h2 class="text-lg font-semibold text-text mb-4">Roaster</h2>
<dl class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
<div>
<dt class="text-text-muted">Name</dt>
<dd class="font-medium text-text">{{ brew.roaster_name }}</dd>
</div>
<div>
<dt class="text-text-muted">Country</dt>
<dd class="font-medium text-text">
{% if !brew.roaster_country_flag.is_empty() %}{{ brew.roaster_country_flag }} {% endif %}{{ brew.roaster_country }}
</dd>
</div>
{% if let Some(city) = brew.roaster_city %}
<div>
<dt class="text-text-muted">City</dt>
<dd class="font-medium text-text">{{ city }}</dd>
</div>
{% endif %}
{% if let Some(url) = brew.roaster_homepage %}
<div>
<dt class="text-text-muted">Website</dt>
<dd class="font-medium">
<a href="{{ url }}" target="_blank" rel="noreferrer noopener" class="text-accent hover:text-accent-hover transition">Visit Website</a>
</dd>
</div>
{% endif %}
</dl>
</div>
<div class="rounded-lg border bg-surface p-5">
<h2 class="text-lg font-semibold text-text mb-4">Gear</h2>
<dl class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
<div>
<dt class="text-text-muted">Grinder</dt>
<dd class="font-medium text-text">{{ brew.grinder_name }}</dd>
</div>
<div>
<dt class="text-text-muted">Brewer</dt>
<dd class="font-medium text-text">{{ brew.brewer_name }}</dd>
</div>
{% if let Some(fp) = brew.filter_paper_name %}
<div>
<dt class="text-text-muted">Filter Paper</dt>
<dd class="font-medium text-text">{{ fp }}</dd>
</div>
{% endif %}
</dl>
</div>
</div>
{# ── Recipe ── #}
<div class="grid gap-6 md:grid-cols-2">
<div class="rounded-lg border bg-surface p-5">
<h2 class="text-lg font-semibold text-text mb-4">Recipe</h2>
<dl class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
<div>
<dt class="text-text-muted">Coffee</dt>
<dd class="font-medium text-text">{{ brew.coffee_weight }}</dd>
</div>
<div>
<dt class="text-text-muted">Water</dt>
<dd class="font-medium text-text">{{ brew.water_volume }}</dd>
</div>
<div>
<dt class="text-text-muted">Temperature</dt>
<dd class="font-medium text-text">{{ brew.water_temp }}</dd>
</div>
<div>
<dt class="text-text-muted">Grind Setting</dt>
<dd class="font-medium text-text">{{ brew.grind_setting }}</dd>
</div>
{% if let Some(time) = brew.brew_time %}
<div>
<dt class="text-text-muted">Brew Time</dt>
<dd class="font-medium text-text">{{ time }}</dd>
</div>
{% endif %}
{% if !brew.quick_notes_label.is_empty() %}
<div>
<dt class="text-text-muted">Notes</dt>
<dd class="font-medium text-text">{{ brew.quick_notes_label }}</dd>
</div>
{% endif %}
</dl>
</div>
</div>
{% endblock %}