Browse docs

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.