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 either an immediate 302 to your destination or, if you enabled buffer page on the link, a minimal HTML interstitial with a short countdown, then the destination — UTMs apply on redirect. Apps that generate link previews get a small HTML page with Open Graph tags (title, description, image) and a minimal visible landing — all from fields you configure — not a dump of your destination URL into the description.
Core behavior
For each GET /[slug] request (canonical short URL), 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. If human_buffer_page is false (default), the response is 302 Found to destinationUrl after merging stored UTM parameters (see Analytics). If human_buffer_page is true, the response is 200 HTML with a countdown and preview snippet, then client-side navigation to the destination (UTMs merged on that URL). A click row is logged when the human request is served in both cases. Use Vary: User-Agent; buffer pages also vary by Accept-Language.
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/slug), not the destination. Older links may use /s/slug; those requests receive a 308 redirect to /slug.
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 and buffer interstitials use private, no-store and Vary: User-Agent (buffer pages add Accept-Language).
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.