Merge pull request #754 from PicPeak/fix/og-shorturl-slideshow-viber-699

fix(og): route branded short URLs + slideshow to OG handler, add Viber (#699)
This commit is contained in:
Paul Nothaft
2026-07-05 23:39:59 +02:00
committed by GitHub
3 changed files with 76 additions and 3 deletions
@@ -343,12 +343,34 @@ describe('isSocialCrawler — extended bot coverage (#521)', () => {
// 3rd-party preview services used by business-messaging stacks
'LinkPreview/1.0',
'Slack-ImgProxy/1.0',
// Viber + broader crawler set (#699 follow-up)
'Mozilla/5.0 (compatible; Viber)',
'Mozilla/5.0 (compatible; Bluesky Cardyb/1.1)',
'facebookcatalog/1.0',
'kakaotalk-scrap/1.0',
'Mozilla/5.0 (compatible; Synapse/1.98)',
'Rocket.Chat/6.0',
];
for (const ua of knownBots) {
expect(isSocialCrawler(ua)).toBe(true);
}
});
it('does NOT match human in-app-browser UAs (our OG response is meta-only, no redirect)', () => {
// These share a token with a preview bot but are also sent by real users
// browsing inside the app's webview — matching them would serve a human
// the bare OG stub. Deliberately excluded; guard against re-adding them.
const inAppBrowsers = [
'Mozilla/5.0 (iPhone) AppleWebKit MicroMessenger/8.0.0', // WeChat in-app
'Mozilla/5.0 (iPhone) AppleWebKit Line/13.0.0', // LINE in-app
'Mozilla/5.0 (Linux; Android) Zalo', // Zalo in-app
'Mozilla/5.0 (Macintosh) Chrome/120.0 Safari/537.36 boxing', // "XING" substring trap
];
for (const ua of inAppBrowsers) {
expect(isSocialCrawler(ua)).toBe(false);
}
});
it('does not match a regular browser UA', () => {
const browsers = [
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36',
+23 -1
View File
@@ -34,7 +34,29 @@ const SOCIAL_CRAWLER_PATTERNS = [
// messaging stacks (Twilio, LinkPreview.net, etc.). Match the
// canonical lowercase substring; the /i flag handles case.
/LinkPreview/i,
/Slack-ImgProxy/i
/Slack-ImgProxy/i,
// Viber's link-preview fetcher — was never detected, so shared links
// showed no rich preview in Viber (#699 follow-up). Keep in sync with the
// UA list in frontend/nginx.conf.
/Viber/i,
// Broader crawler coverage (#699 follow-up, from alexvaltchev's field list).
// IMPORTANT: only CRAWLER-EXCLUSIVE tokens are added here. Our OG response is
// meta-only (no client redirect), so a UA shared with a real human in-app
// browser would serve that human the bare stub. That rules out WeChat
// (MicroMessenger), LINE (Line/), Zalo, and generic strings like
// "InAppBrowser"/"preview"/"unfurl" — deliberately NOT added.
/Cardyb/i, // Bluesky's link-card service (the actual fetcher UA)
/facebookcatalog/i, // Facebook catalog crawler
/Signal/i, // Signal link preview
/Misskey/i, // fediverse (server-side preview fetch)
/Pleroma/i, // fediverse
/Synapse/i, // Matrix homeserver URL preview
/Nextcloud/i, // Nextcloud Talk/News link crawler
/Rocket\.Chat/i, // Rocket.Chat server preview
/kakaotalk-scrap/i, // KakaoTalk's scraper (NOT the in-app browser UA)
/Google-PageRenderer/i, // Google Chat previews (not Search)
/OdklBot/i, // Odnoklassniki
/ZoomBot/i // Zoom Team Chat
];
function isSocialCrawler(userAgent) {
+31 -2
View File
@@ -237,12 +237,25 @@ server {
# 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_-]+)(?:/[^/]+)?/?$ {
# {0,2} extra path segments so the deeper gallery share shapes match too:
# /gallery/<slug> (public / share-token link)
# /gallery/<slug>/client-access (client-access; token is in ?query)
# /gallery/<slug>/show/<token> (slideshow; token is a PATH segment)
# The slideshow shape has TWO extra segments (show + token) — the old
# single-segment `(?:/[^/]+)?` never matched it, so slideshow links fell
# through to the SPA and got only the generic site-wide OG (#699 follow-up).
# NB: the regex is QUOTED because the {0,2} quantifier's braces would
# otherwise be parsed as nginx config block delimiters.
location ~ "^/gallery/(?<gallery_slug>[A-Za-z0-9_-]+)(?:/[^/]+){0,2}/?$" {
# Keep this list in sync with SOCIAL_CRAWLER_PATTERNS in
# backend/src/services/galleryOgService.js. WhatsAppBot / wa-bot
# and LinkPreview / Slack-ImgProxy added in #521 to catch
# business-API preview fetchers that aren't the main WhatsApp app.
if ($http_user_agent ~* "(facebookexternalhit|facebot|Twitterbot|WhatsApp|WhatsAppBot|wa-bot|Slackbot|Slack-ImgProxy|TelegramBot|SkypeUriPreview|Discordbot|LinkedInBot|Pinterest|vkShare|redditbot|Embedly|iframely|Snapchat|Applebot|Mastodon|Bluesky|OpenGraph|LinkPreview)") {
# Viber + the broader set below added in #699 follow-up. Only
# CRAWLER-EXCLUSIVE tokens — the backend OG response is meta-only (no
# redirect), so UAs shared with real human in-app browsers (WeChat's
# MicroMessenger, LINE's "Line/", Zalo, "InAppBrowser") are NOT added.
if ($http_user_agent ~* "(facebookexternalhit|facebookcatalog|facebot|Twitterbot|WhatsApp|WhatsAppBot|wa-bot|Slackbot|Slack-ImgProxy|TelegramBot|SkypeUriPreview|Discordbot|LinkedInBot|Pinterest|vkShare|redditbot|Embedly|iframely|Snapchat|Applebot|Mastodon|Bluesky|Cardyb|OpenGraph|LinkPreview|Viber|Signal|Misskey|Pleroma|Synapse|Nextcloud|Rocket\.Chat|kakaotalk-scrap|Google-PageRenderer|OdklBot|ZoomBot)") {
rewrite ^ /og/gallery/$gallery_slug last;
}
try_files $uri $uri/ /index.html;
@@ -260,6 +273,22 @@ server {
proxy_set_header X-Forwarded-Proto $real_proto;
}
# Branded URL shortener (#699). The backend `/s/<short_slug>` route both
# 302-redirects humans to the target gallery AND server-renders OG for
# social crawlers. Without this proxy, `/s/...` fell through to the SPA
# (which has no /s/ route) — so branded short links were dead for humans
# and crawlers alike. `^~` beats the regex SPA fallback below. The backend
# does its own UA detection, so no crawler `if` is needed here.
location ^~ /s/ {
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 $real_proto;
}
# SPA fallback
location / {
try_files $uri $uri/ /index.html;