Link previews & social cards
Technical reference for how requests are split between preview bots and normal browsers. For a plain-language summary, see How your links work.
In short: Normal browsers get a redirect to your real page. Apps that generate link previews get a small HTML page with Open Graph tags (title, description, image) that you configure — not a dump of your destination URL into the description.
Core behavior
For each GET /s/[slug] request, the handler inspects User-Agent. If it matches a known link-preview / social crawler pattern in lib/crawler-uas.ts, the response is 200 HTML with Open Graph and Twitter meta tags built from stored botTitle, botDescription, and optional ogImageUrl.
Otherwise the client is treated as a normal browser: 302 Found to destinationUrl (after merging stored UTM parameters — see Analytics).
Canonical and og:url
Bot HTML sets og:url and link rel="canonical" to the short URL on your app origin (e.g. https://your.app/s/slug), not the destination.
Non-negotiable content rules
Never put the real destinationUrl (or an equivalent leak of the final landing URL) into bot-facing HTML or og:description. Crawlers and preview cards must only expose the title, description, and image you intend for the tile.
The quick shorten path uses a generic description string so the true URL never appears in og:description.
Inactive and expired links
If isActive is false or expiresAt is in the past, the route returns 410 Gone (both bots and humans).
Caching
Bot responses use Cache-Control: public, max-age=300, ... and Vary: User-Agent. Human redirects use private, no-store.
Extending crawler detection
Adding UA substrings reduces false negatives (bots hitting 302 and seeing Location), but can increase false positives (real users misclassified as bots). Change lib/crawler-uas.ts carefully and regression-test with real preview debuggers. See Crawler checklist and Troubleshooting.