Skip to content

Deep links

The problem this service was built for: a YouTube link in an Instagram bio opens inside Instagram’s own in-app browser, not the YouTube app. The visitor is not logged in, cannot subscribe, and usually leaves.

There is no single trick that fixes this. What works depends on the operating system and on whether the tap happened inside a webview — so the worker reads the user-agent and picks one of four branches.

Situation Branch What is served
Bot / crawler plain 302 to the destination
Android, recipe has an intent intent 302 to intent://…
iOS, not in a webview universal 302 to the plain https URL
iOS, inside a webview interstitial 200, an HTML page with a tap target
Anything else plain 302 to the destination

Every branch’s reasoning, in order:

  • Bots get the plain redirect. A crawler in an interstitial breaks link previews, and an intent:// URL means nothing to it.
  • Android gets intent://. It is the only genuinely silent path: S.browser_fallback_url makes Android open the web page itself when the app is missing — no error, no intermediate page.
  • iOS outside a webview gets the URL untouched. Safari triggers the Universal Link natively. Forcing a custom scheme here would pop a useless alert for anyone who doesn’t have the app.
  • iOS inside a webview gets a page. WKWebView does not intercept Universal Links on direct navigation: without a user tap, nothing opens. This is the only branch that renders HTML instead of redirecting, and it is served cache-control: no-store — the answer depends on the user-agent, so nothing about it is cacheable.

The webviews that get this treatment are the ones that actually break Universal Links: Instagram, TikTok, Facebook, LinkedIn, X, Snapchat, Pinterest.

A recipe is matched from the destination hostname, so an ordinary link with deeplink: null already deep-links. Adding an app means adding one file.

App iOS Android package
YouTube vnd.youtube://… com.google.android.youtube
Instagram instagram://user?username=… (profiles only) com.instagram.android
TikTok (none — Universal Link) com.zhiliaoapp.musically
X / Twitter twitter://user?screen_name=… or twitter://status?id=… com.twitter.android
Reddit reddit://… com.reddit.frontpage
Spotify spotify:type:id com.spotify.music

Three of those gaps are decisions, not omissions:

  • YouTube uses vnd.youtube://, not youtube:// — that is the scheme the app actually declares.
  • TikTok has no iOS scheme on purpose. Its internal schemes (snssdk1233://) change between versions and fail silently. Better to let the Universal Link try and gain nothing than to send someone into a dead end.
  • Instagram only gets an iOS scheme for profiles. There is no generic Instagram scheme that accepts a web URL. For a post or a reel, iOS falls back to the Universal Link and Android still gets its intent.

X keeps the twitter:// scheme after the rename, because the app does.

The deeplink field on a link overrides the recipe. A partial override completes the recipe rather than replacing it — you can fix iOS without rewriting the Android intent:

{ "deeplink": { "ios": "vnd.youtube://www.youtube.com/@VincentLeSerpent" } }
Key Effect
enabled false disables deep linking for this link entirely
ios iOS scheme URL
android full intent:// URL
app the app name shown on the interstitial (default: l’application)

Parameters are merged onto the destination, weakest to strongest:

  1. parameters already in the destination URL
  2. the link’s configured utm
  3. parameters on the short link that was clicked

The click wins, because it carries the real context: 301.so/yt?utm_content=story must be able to override the stored default.

That an app actually opens. Instagram and TikTok webview behaviour is an OS behaviour, not something a user-agent string simulates. The test suite proves the worker decides correctly; the opening itself is tested on a phone, and diagnosed with /_debug when it breaks.