fix(share): OG/Twitter-card metadata for gallery share URLs (#333)
WhatsApp / Slack / Facebook / Twitter previews showed nothing useful for shared gallery links — the SPA's stub index.html has no OG tags and the meta-injection in DynamicFavicon happens at runtime, which crawlers never see (they don't execute JS). Add a backend OG handler at /og/gallery/:slug that returns minimal HTML with proper og:* and twitter:* meta sourced from the event row + branding settings (event name, formatted date, welcome_message excerpt as description, configured logo as the preview image, FRONTEND_URL-based canonical). Honours slug redirects so renamed galleries still get rich previews. Wire crawler detection in both nginx configs (production and dev) — UA match against the standard list (facebookexternalhit, WhatsApp, Slackbot, Twitterbot, Discordbot, LinkedInBot, etc.) triggers an internal rewrite to /og/gallery/:slug, while humans fall through to the SPA via try_files. The OG endpoint is also wired into the native-install SPA fallback in server.js for setups that bypass nginx. The OG image is intentionally the brand logo, not a gallery photo — crawlers fetch it without auth, and password-protected gallery photos must not leak via share previews.
This commit is contained in:
@@ -148,6 +148,29 @@ server {
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# Social-crawler detection for gallery share URLs. Crawlers (WhatsApp,
|
||||
# Facebook, Slack, Twitter, etc.) don't run JS, so the SPA's client-side
|
||||
# meta tags never reach them. Route those UAs to backend's /og handler
|
||||
# via internal rewrite; humans fall through to the SPA via try_files.
|
||||
location ~ ^/gallery/(?<gallery_slug>[A-Za-z0-9_-]+)(?:/[^/]+)?/?$ {
|
||||
if ($http_user_agent ~* "(facebookexternalhit|facebot|Twitterbot|WhatsApp|Slackbot|TelegramBot|SkypeUriPreview|Discordbot|LinkedInBot|Pinterest|vkShare|redditbot|Embedly|iframely|Snapchat|Applebot|Mastodon|Bluesky|OpenGraph)") {
|
||||
rewrite ^ /og/gallery/$gallery_slug last;
|
||||
}
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# OG preview endpoint (proxied to backend). Public endpoint by design —
|
||||
# only exposes event_name + branding logo, no protected photo content.
|
||||
location ^~ /og/gallery/ {
|
||||
set $backend_upstream backend;
|
||||
proxy_pass http://$backend_upstream:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# SPA fallback
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
@@ -27,6 +27,23 @@ server {
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
|
||||
# Gallery share URLs: route social-crawler UAs to backend OG handler.
|
||||
location ~ ^/gallery/(?<gallery_slug>[A-Za-z0-9_-]+)(?:/[^/]+)?/?$ {
|
||||
if ($http_user_agent ~* "(facebookexternalhit|facebot|Twitterbot|WhatsApp|Slackbot|TelegramBot|SkypeUriPreview|Discordbot|LinkedInBot|Pinterest|vkShare|redditbot|Embedly|iframely|Snapchat|Applebot|Mastodon|Bluesky|OpenGraph)") {
|
||||
rewrite ^ /og/gallery/$gallery_slug last;
|
||||
}
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location ^~ /og/gallery/ {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# SPA fallback
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
Reference in New Issue
Block a user