From 9e5dff7f5f4f008c65ee73d9e0c435afb29f449c Mon Sep 17 00:00:00 2001 From: Jon Seager Date: Sun, 8 Feb 2026 16:19:34 +0000 Subject: [PATCH] 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 --- CLAUDE.md | 13 +++++++++++++ src/application/routes/app/brews.rs | 1 + src/application/routes/app/cups.rs | 1 + src/application/routes/app/home.rs | 1 + src/application/routes/app/mod.rs | 11 +++++++++++ src/application/routes/app/stats.rs | 1 + src/lib.rs | 12 ++++++++++++ src/main.rs | 2 ++ src/presentation/web/templates.rs | 4 ++++ static/og-image.png | Bin 0 -> 35606 bytes templates/base.html | 5 +++++ templates/pages/brew.html | 3 +++ templates/pages/cup.html | 3 +++ templates/pages/home.html | 5 +++-- templates/pages/stats.html | 3 +++ 15 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 static/og-image.png diff --git a/CLAUDE.md b/CLAUDE.md index 3b518a9..a51ea40 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -140,6 +140,19 @@ When adding new pragmas, add them after the existing ones in `Database::connect( The middleware stack in `application/routes/mod.rs` applies layers in this order (outermost first): request tracing, cookie parsing, body size limit, security headers (`X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `CSP`, `HSTS`), and gzip compression. When adding new middleware, place it in the `ServiceBuilder` chain at the appropriate position. +### Open Graph Meta Tags + +Social media preview cards are powered by Open Graph and Twitter Card meta tags in `templates/base.html`. The base template defines default `og:title`, `og:description`, `og:type`, `og:site_name`, and `twitter:card` tags using Askama blocks (`{% block og_title %}`, `{% block og_description %}`). + +**Base URL** — `BREWLOG_RP_ORIGIN` is stored at startup via `set_base_url()` in `lib.rs` (a `OnceLock`). Templates access it through the `base_url` field on their template struct, set with `crate::base_url()` in the handler. + +**`og:image`** — a static 1200x630 PNG (`static/og-image.png`) served at `/og-image.png` via `include_bytes!()`. Only public shareable pages (home, brew detail, cup detail, stats) include it via `{% block head %}`, since those are the pages social crawlers can access. + +**Adding OG tags to a new page:** +1. Add `pub base_url: &'static str` to the template struct +2. Set `base_url: crate::base_url()` in the handler +3. Override `{% block og_title %}`, `{% block og_description %}`, and add `` in `{% block head %}` + ### Repository Pattern All data access goes through trait-based repositories defined in `domain/repositories.rs`. SQL implementations live in `infrastructure/repositories/`, each using a private `Record` struct with a `to_domain()` method to convert database rows to domain entities. Use typed ID wrappers from `domain/ids.rs` (e.g., `RoastId`, `BagId`) — never raw `i64`. diff --git a/src/application/routes/app/brews.rs b/src/application/routes/app/brews.rs index f69f18b..8d11ba7 100644 --- a/src/application/routes/app/brews.rs +++ b/src/application/routes/app/brews.rs @@ -48,6 +48,7 @@ pub(crate) async fn brew_detail_page( nav_active: "", is_authenticated, version_info: &crate::VERSION_INFO, + base_url: crate::base_url(), brew: view, }; diff --git a/src/application/routes/app/cups.rs b/src/application/routes/app/cups.rs index 7335831..e0a514e 100644 --- a/src/application/routes/app/cups.rs +++ b/src/application/routes/app/cups.rs @@ -53,6 +53,7 @@ pub(crate) async fn cup_detail_page( nav_active: "", is_authenticated, version_info: &crate::VERSION_INFO, + base_url: crate::base_url(), cup: view, }; diff --git a/src/application/routes/app/home.rs b/src/application/routes/app/home.rs index 3108ca8..8e0751f 100644 --- a/src/application/routes/app/home.rs +++ b/src/application/routes/app/home.rs @@ -43,6 +43,7 @@ pub(crate) async fn home_page( nav_active: "home", is_authenticated, version_info: &crate::VERSION_INFO, + base_url: crate::base_url(), recent_brews: content.recent_brews, open_bags: content.open_bags, recent_events: content.recent_events, diff --git a/src/application/routes/app/mod.rs b/src/application/routes/app/mod.rs index 8ed907a..bad1adb 100644 --- a/src/application/routes/app/mod.rs +++ b/src/application/routes/app/mod.rs @@ -43,6 +43,7 @@ pub(super) fn router() -> axum::Router { .route("/components/donut-chart.js", get(donut_chart_js)) .route("/favicon-light.svg", get(favicon_light)) .route("/favicon-dark.svg", get(favicon_dark)) + .route("/og-image.png", get(og_image)) .route("/health", get(health)) } @@ -140,6 +141,16 @@ async fn favicon_dark() -> impl IntoResponse { ) } +async fn og_image() -> impl IntoResponse { + ( + [ + ("content-type", "image/png"), + ("cache-control", "public, max-age=604800"), + ], + include_bytes!("../../../../static/og-image.png").as_slice(), + ) +} + async fn health() -> impl IntoResponse { ([("content-type", "application/json")], r#"{"status":"ok"}"#) } diff --git a/src/application/routes/app/stats.rs b/src/application/routes/app/stats.rs index 2f53af3..ace6526 100644 --- a/src/application/routes/app/stats.rs +++ b/src/application/routes/app/stats.rs @@ -125,6 +125,7 @@ pub(crate) async fn stats_page( nav_active: "stats", is_authenticated, version_info: &crate::VERSION_INFO, + base_url: crate::base_url(), active_type: entity_type, tabs, tab_signal: "_active-tab", diff --git a/src/lib.rs b/src/lib.rs index 6ac662d..e27a147 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +use std::sync::OnceLock; + pub mod application; pub mod domain; pub mod infrastructure; @@ -12,3 +14,13 @@ pub const VERSION_INFO: VersionInfo = VersionInfo { version: env!("CARGO_PKG_VERSION"), commit: env!("GIT_HASH"), }; + +static BASE_URL: OnceLock = OnceLock::new(); + +pub fn set_base_url(url: String) { + let _ = BASE_URL.set(url); +} + +pub fn base_url() -> &'static str { + BASE_URL.get().map_or("", std::string::String::as_str) +} diff --git a/src/main.rs b/src/main.rs index 64bcc8b..fab38e9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -98,6 +98,8 @@ async fn run_server(command: ServeCommand) -> Result<()> { ) })?; + brewlog::set_base_url(rp_origin.clone()); + let config = ServerConfig { bind_address: command.bind_address, database_url: command.database_url, diff --git a/src/presentation/web/templates.rs b/src/presentation/web/templates.rs index 72bc0fb..da5d3be 100644 --- a/src/presentation/web/templates.rs +++ b/src/presentation/web/templates.rs @@ -105,6 +105,7 @@ pub struct HomeTemplate { pub nav_active: &'static str, pub is_authenticated: bool, pub version_info: &'static crate::VersionInfo, + pub base_url: &'static str, pub recent_brews: Vec, pub open_bags: Vec, @@ -183,6 +184,7 @@ pub struct StatsPageTemplate { pub nav_active: &'static str, pub is_authenticated: bool, pub version_info: &'static crate::VersionInfo, + pub base_url: &'static str, pub active_type: String, pub tabs: Vec, pub tab_signal: &'static str, @@ -214,6 +216,7 @@ pub struct BrewDetailTemplate { pub nav_active: &'static str, pub is_authenticated: bool, pub version_info: &'static crate::VersionInfo, + pub base_url: &'static str, pub brew: BrewDetailView, } @@ -223,6 +226,7 @@ pub struct CupDetailTemplate { pub nav_active: &'static str, pub is_authenticated: bool, pub version_info: &'static crate::VersionInfo, + pub base_url: &'static str, pub cup: CupDetailView, } diff --git a/static/og-image.png b/static/og-image.png new file mode 100644 index 0000000000000000000000000000000000000000..f3f1b9fd6eb42f61da8bff3977e78515263cd102 GIT binary patch literal 35606 zcmeFZS5#B^_XirsQGPaXq0jZH1dMu+vs)7`0Q3NTW zw?Gnzg(AIo0@8cvHSM0D^Ivz}$NM~a2}xKv?R)nAw4GOX4YW@kKX)7i0-e&i4ZQ~f z{ayqD{pS0}QQ#e+jw~VI>4M$ud-@S;q+2ajXsJ0rk4ytXjX*M>@2rAV)XX%|Nhsd^3OTlb;bpmg6Y$tEq9(Q?jQD#^>6nw zaJU>x+EEq#nEj|RX0S9o>^ES$2ahLY5n0NSe_w&}^4Z7o>+$EMC;#`!>kEe@{(b++ z@5de-`uD}9+n^Kw{(a%*Z|4904LbJc=l|Qv?bjfle?R!^*?*h(??iz>|E(wx=)d~{ z1p2RlfI$CMTF`&}0%-jIHCE7n@Bl!M|KX_r-~oV_|APnrzuPS}lIf%EnkMADEzZ;p$a>TQeUhh8b4s;m zs_Inxot-6sOFi*1(lY9d5h-=#<<2Dj0Rp|JC)2in7^{y$Y}f6iS7>)LG5+15XM%?m z)8~fLRl+OoSPA@Q{&~n@`X7-(34$EN_v-oT^&eA_#^V^V!Cu1%jNbLOmR(g!slhl2 zKD7j2PQv(!mg>)x&Zqf6$H$Mf#=8m`ZEl4bCyd3_`X`jG>?~MzgG|$NM+fQq70r-y z)itvxv_rr~tZ=ItL3#kR-hrE%@25pDJJN@2_I)Rh9C84k@I7a>M_d4(&=Bc9;Rh~x8mczKW%BtVOtm`H_PG}6Qu`q}M?gToO?MU8WO;1l?BDo(o$aClqFBz_jzpAoH1C&y&|~2Lw&P2D>do)qu zT&p36&^CF9*j5|=U0R8^VJ_X$h05#Hx6IUb2r0P^p}l+gJjqZOB~Mmd`mDDvRHI)z zB3n~!_xYcx$nL$$MZ?8FXXxl$Bx-p5nVdo8jYL`<7ppSccoxGgDz#Al9Z zghzmlRz5FdhgK5h+K2-`gbnW(G2Fo6hKb%C11il-x=3Mj#f`NWZlTrOMj!m!YG8#r zJ9%aZ!f)o}xbF0|cz#vhVxz?k4vPPMXnCfa^{}5G0cT0e+@Ce|M>Dg`J!XgZ^k0rL zU=+;YVY1&I<&?04W3b2HEM2gki=l?=gyX!__4F>qDvrJ-ULzNx;OufG0=m2taAzdv zCN%#zg+*k%1M}4h>|POyAJy>b`zIZvWEL#H#JE{UE)+Z?!^-+#S2J=c{V{o3JpPmc zDvoTnzL8bWKJ+$)M<((k#cc-44KXzhAzya0FcdTeI#4ajj@gFCFPjV~|O z>^CDZE8$4=t)F|9+Lnv;pbJ z4eMej<_WJrZHXGME;~EzOUM-Ge!S|3Z{=X_g4X%FTdi^iev#k7 znn6}0N)EeJRt+n=5W-{4nB*91*FzN-Ie5~%{cwQ>>kaSFbE^~%DV-fxy~9R1*I6Al zM6UZuREvb8KVl^D$cDEg*gjqnfqPo_LtwL$0Z<7y<>`{O`8(IX4rTA2QV|j8IGP%C zpW=31xZbyhyvB`BIBL*m+PvSos6<;pR9i1UUV5VjhtluQ&lBAP3*{bb@m;OaXuZM= z{L+6qn(RQ777n?w|Eaxw_oD6S+8CBu=d{+ma(XdhVoze%uVwF6s8&QOinW$NKD*YN z73#BGVOPt=B8BYd1ZuS;t7oj1*33)sfS!!8Rm%7GuYP}5Ro4>?sZZPgyknMjdad

nY*sQo{d%X(vZg;B<6$hg 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 literal 0 HcmV?d00001 diff --git a/templates/base.html b/templates/base.html index 4d4d899..7923232 100644 --- a/templates/base.html +++ b/templates/base.html @@ -7,6 +7,11 @@ name="description" content="{% block description %}Self-hosted coffee logging — track roasters, roasts, bags, brews, and gear.{% endblock %}" /> + + + + + {% block title %}Brewlog{% endblock %} diff --git a/templates/pages/brew.html b/templates/pages/brew.html index aefc0b6..84504c9 100644 --- a/templates/pages/brew.html +++ b/templates/pages/brew.html @@ -2,6 +2,9 @@ {% 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 %}{% endblock %} {% block content %}

diff --git a/templates/pages/cup.html b/templates/pages/cup.html index 9ee49f8..b147a03 100644 --- a/templates/pages/cup.html +++ b/templates/pages/cup.html @@ -2,6 +2,9 @@ {% import "partials/icons.html" as icons %} {% block title %}Brewlog · {{ cup.roast_name }} at {{ cup.cafe_name }}{% endblock %} {% block description %}{{ cup.roast_name }} by {{ cup.roaster_name }} at {{ cup.cafe_name }}, {{ cup.cafe_city }}.{% endblock %} +{% block og_title %}{{ cup.roast_name }} at {{ cup.cafe_name }} — Brewlog{% endblock %} +{% block og_description %}{{ cup.roast_name }} by {{ cup.roaster_name }} at {{ cup.cafe_name }}, {{ cup.cafe_city }}.{% endblock %} +{% block head %}{% endblock %} {% block content %}
diff --git a/templates/pages/home.html b/templates/pages/home.html index 8c28881..ef51c67 100644 --- a/templates/pages/home.html +++ b/templates/pages/home.html @@ -1,6 +1,7 @@ {% extends "base.html" %} {% import "partials/bag_card.html" as bag_card %} {% import -"partials/brew_card.html" as brew_card %} {% import "partials/icons.html" as icons %} {% block title %}Brewlog{% endblock %} {% block head %} {% if -is_authenticated %} +"partials/brew_card.html" as brew_card %} {% import "partials/icons.html" as icons %} {% block title %}Brewlog{% endblock %} {% block head %} + +{% if is_authenticated %}