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.
The decision matrix
Section titled “The decision matrix”| 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_urlmakes 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.
WKWebViewdoes 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 servedcache-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.
The six recipes
Section titled “The six recipes”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://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://… |
com.reddit.frontpage |
|
| Spotify | spotify:type:id |
com.spotify.music |
Three of those gaps are decisions, not omissions:
- YouTube uses
vnd.youtube://, notyoutube://— 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.
Overriding it
Section titled “Overriding it”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) |
Query parameters
Section titled “Query parameters”Parameters are merged onto the destination, weakest to strongest:
- parameters already in the destination URL
- the link’s configured
utm - 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.
What cannot be tested from a terminal
Section titled “What cannot be tested from a terminal”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.