{"id":436,"date":"2026-03-13T17:00:00","date_gmt":"2026-03-13T22:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=436"},"modified":"2026-03-07T04:45:29","modified_gmt":"2026-03-07T09:45:29","slug":"deep-linking-for-web","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/deep-linking-for-web\/","title":{"rendered":"Deep Linking for Web: Bridging Browser and App Experiences"},"content":{"rendered":"\n<p>Your app has a product detail page that loads in under a second, handles offline mode gracefully, and sends push notifications when a user&#39;s order ships. Your mobile website has the same content but it&#39;s slower, harder to navigate, and lacks those features. Yet a large chunk of your mobile users browse your site in a browser and never make it into your app.<\/p>\n\n\n\n<p>This is the web-to-app gap, and it costs real conversions. Deep linking for web is the set of techniques that closes it: detecting whether the app is installed, routing users into the right screen when it is, and providing a useful fallback experience when it is not.<\/p>\n\n\n\n<p>This article covers the full range of strategies, from the basic HTML meta tag to JavaScript detection APIs, server-side logic, and landing pages that work as graceful fallbacks.<\/p>\n\n\n\n<p><img decoding=\"async\" src=\"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/screenshot-routes-1772819856524.png\" alt=\"Tolinku dashboard showing route configuration for deep links\">\n<em>The route list page showing all configured deep link routes with paths, types, and actions.<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Web-to-App Problem in Practice<\/h2>\n\n\n\n<p>Consider a few common scenarios:<\/p>\n\n\n\n<p>A user clicks a product link in a marketing email. They open it on their phone. They have your app installed. The link opens in their mobile browser, they browse the product, and they leave. They never saw the smoother in-app experience, and your push notification for abandoned carts never fires because the session was in the browser.<\/p>\n\n\n\n<p>A user searches for your brand on Google and taps an organic result. They land on your mobile site. No banner tells them an app exists. They convert on the web, but their data is siloed and you cannot retarget them with app-specific campaigns.<\/p>\n\n\n\n<p>A user shares a link to a product with a friend. The friend taps it, sees your mobile web page, and eventually finds the app store listing on their own, if at all. The context from that shared link (the specific product, maybe a referral code) is lost.<\/p>\n\n\n\n<p>Each of these scenarios has a solution, and the solutions layer together. None of them require replacing your website. The approach is progressive enhancement: the web experience works fine on its own, and you add layers to route app users into the app while converting non-app users.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Apple Smart App Banner Meta Tag<\/h2>\n\n\n\n<p>The quickest way to surface your app on iOS Safari is the <code>apple-itunes-app<\/code> meta tag. Add it to your HTML <code>&lt;head&gt;<\/code> and Safari renders a small banner at the top of the page with your app icon, name, and a button to open or install the app.<\/p>\n\n\n\n<pre><code class=\"language-html\">&lt;meta name=&quot;apple-itunes-app&quot; content=&quot;app-id=YOUR_APP_STORE_ID&quot;&gt;\n<\/code><\/pre>\n\n\n\n<p>If you want the banner to open a specific screen when tapped, add the <code>app-argument<\/code> parameter:<\/p>\n\n\n\n<pre><code class=\"language-html\">&lt;meta\n  name=&quot;apple-itunes-app&quot;\n  content=&quot;app-id=YOUR_APP_STORE_ID, app-argument=https:\/\/yourapp.com\/products\/42&quot;\n&gt;\n<\/code><\/pre>\n\n\n\n<p>Your app receives the <code>app-argument<\/code> value in the <code>application(_:open:options:)<\/code> delegate method and can parse it to navigate to the right screen. This is Apple&#39;s simplest version of deep linking from the web.<\/p>\n\n\n\n<p>The limitations are real. The banner only works in Safari, not Chrome or other browsers on iOS. You cannot customize the banner&#39;s appearance. You get no analytics on how many users tapped it. And there is nothing equivalent built into Android.<\/p>\n\n\n\n<p>For many teams, that is enough to start. For teams that need more control, custom banners fill the gaps. <a href=\"https:\/\/tolinku.com\/features\/smart-banners\">Tolinku&#39;s smart banner feature<\/a> provides a cross-platform custom banner you can configure without building it yourself, including targeting rules, scheduling, and click tracking.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Custom Smart Banners with JavaScript<\/h2>\n\n\n\n<p>If you want a banner that works on Android, works in Chrome on iOS, and gives you control over appearance and behavior, you build it yourself or use a library. The basic mechanics are straightforward.<\/p>\n\n\n\n<pre><code class=\"language-html\">&lt;!-- Banner markup (hidden by default) --&gt;\n&lt;div id=&quot;app-banner&quot; style=&quot;display: none;&quot;&gt;\n  &lt;img src=&quot;\/app-icon.png&quot; alt=&quot;App icon&quot;&gt;\n  &lt;div&gt;\n    &lt;strong&gt;YourApp&lt;\/strong&gt;\n    &lt;span&gt;Open for the best experience&lt;\/span&gt;\n  &lt;\/div&gt;\n  &lt;a id=&quot;app-banner-btn&quot; href=&quot;#&quot;&gt;Open App&lt;\/a&gt;\n  &lt;button id=&quot;app-banner-close&quot;&gt;\u2715&lt;\/button&gt;\n&lt;\/div&gt;\n<\/code><\/pre>\n\n\n\n<pre><code class=\"language-javascript\">(function() {\n  var ua = navigator.userAgent;\n  var isIOS = \/iPhone|iPad|iPod\/.test(ua);\n  var isAndroid = \/Android\/.test(ua);\n\n  if (!isIOS &amp;&amp; !isAndroid) return; \/\/ Desktop: no banner\n\n  var banner = document.getElementById(&#39;app-banner&#39;);\n  var btn = document.getElementById(&#39;app-banner-btn&#39;);\n  var close = document.getElementById(&#39;app-banner-close&#39;);\n\n  \/\/ Set the deep link URL based on current page\n  var deepLinkPath = window.location.pathname + window.location.search;\n  if (isIOS) {\n    btn.href = &#39;https:\/\/apps.apple.com\/app\/idYOUR_APP_ID&#39;;\n  } else {\n    btn.href = &#39;https:\/\/play.google.com\/store\/apps\/details?id=com.yourapp&#39;;\n  }\n\n  banner.style.display = &#39;flex&#39;;\n\n  close.addEventListener(&#39;click&#39;, function() {\n    banner.style.display = &#39;none&#39;;\n    \/\/ Store in sessionStorage so the banner doesn&#39;t reappear\n    sessionStorage.setItem(&#39;app-banner-dismissed&#39;, &#39;1&#39;);\n  });\n\n  if (sessionStorage.getItem(&#39;app-banner-dismissed&#39;)) {\n    banner.style.display = &#39;none&#39;;\n  }\n})();\n<\/code><\/pre>\n\n\n\n<p>This is a starting point. In production you would also handle the &quot;open app if installed&quot; case with a URI scheme attempt, track clicks, and persist the dismissal in localStorage so users who have already seen and dismissed the banner do not see it again on their next visit.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Attempting to Open the App Directly<\/h2>\n\n\n\n<p>The banner above links to the App Store or Google Play. That is safe: it always works. But if the user already has the app installed, you want to open the app directly instead.<\/p>\n\n\n\n<p>The traditional way to do this on the web is a custom URI scheme attempt. You write a deep link URL to your app&#39;s custom scheme, try to navigate to it, and see if anything happens.<\/p>\n\n\n\n<pre><code class=\"language-javascript\">function tryOpenApp(deepLinkUrl, fallbackUrl, timeout) {\n  timeout = timeout || 1500;\n  var start = Date.now();\n\n  \/\/ Try the custom scheme\n  window.location.href = deepLinkUrl; \/\/ e.g. &quot;yourapp:\/\/products\/42&quot;\n\n  \/\/ If the app is not installed, nothing happens and we land here\n  setTimeout(function() {\n    \/\/ If the page is still visible (not hidden by app switch), redirect to fallback\n    if (Date.now() - start &lt; timeout + 200) {\n      window.location.href = fallbackUrl;\n    }\n  }, timeout);\n}\n<\/code><\/pre>\n\n\n\n<p>The logic: you try to navigate to the custom scheme. If the app is installed, the OS hands off to the app and the user leaves the browser. If the app is not installed, nothing happens, and after the timeout you redirect to the fallback (usually the App Store or a landing page).<\/p>\n\n\n\n<p>The timeout approach has well-known reliability issues. Some browsers fire the fallback redirect even when the app does launch. Some OSes show a confirmation dialog that pauses the timer. iOS Safari in particular has changed its behavior for custom scheme handling across versions. Custom schemes are also not indexed by search engines and do not benefit from Universal Link or App Link trust.<\/p>\n\n\n\n<p>For more reliable behavior, use Universal Links on iOS and App Links on Android as your primary mechanism, with custom schemes only as a fallback for older OS versions.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The <code>getInstalledRelatedApps<\/code> API<\/h2>\n\n\n\n<p>The <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/Manifest\" rel=\"nofollow noopener\" target=\"_blank\">Web Application Manifest<\/a> and the <code>getInstalledRelatedApps<\/code> API offer a more direct way to check whether your app is installed, at least on Android with Chrome.<\/p>\n\n\n\n<p>First, add your app to your web app manifest:<\/p>\n\n\n\n<pre><code class=\"language-json\">{\n  &quot;name&quot;: &quot;YourApp&quot;,\n  &quot;related_applications&quot;: [\n    {\n      &quot;platform&quot;: &quot;play&quot;,\n      &quot;url&quot;: &quot;https:\/\/play.google.com\/store\/apps\/details?id=com.yourapp&quot;,\n      &quot;id&quot;: &quot;com.yourapp&quot;\n    }\n  ],\n  &quot;prefer_related_applications&quot;: false\n}\n<\/code><\/pre>\n\n\n\n<p>Then query it from JavaScript:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">async function checkAppInstalled() {\n  if (!(&#39;getInstalledRelatedApps&#39; in navigator)) {\n    return false; \/\/ API not supported\n  }\n  try {\n    var apps = await navigator.getInstalledRelatedApps();\n    return apps.some(function(app) {\n      return app.id === &#39;com.yourapp&#39;;\n    });\n  } catch (e) {\n    return false;\n  }\n}\n\ncheckAppInstalled().then(function(installed) {\n  if (installed) {\n    \/\/ Show &quot;Open in App&quot; button with direct deep link\n    showOpenInAppButton();\n  } else {\n    \/\/ Show &quot;Get the App&quot; banner\n    showInstallBanner();\n  }\n});\n<\/code><\/pre>\n\n\n\n<p>As of 2026, <code>getInstalledRelatedApps<\/code> works reliably on Chrome for Android. It is not available on iOS, where Safari does not implement it. The API requires that your app declares your website in its asset links (the <code>assetlinks.json<\/code> file at <code>\/.well-known\/assetlinks.json<\/code>), so there is a two-way trust requirement, which is the same verification Android App Links need anyway.<\/p>\n\n\n\n<p>This API is genuinely useful for Android. It lets you show the right CTA without ambiguity: if the app is installed, offer to open it directly; if not, offer to install it. No timeout hacks required.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Server-Side User Agent Detection<\/h2>\n\n\n\n<p>Not everything needs to happen in JavaScript. Server-side user agent detection is useful for redirects that should happen before the page renders at all.<\/p>\n\n\n\n<p>If you run a marketing site and want mobile users to land on a page optimized for app conversion while desktop users see the standard page, you can branch at the server or CDN level.<\/p>\n\n\n\n<p>In an Express app, for example:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">const mobileRe = \/Android|iPhone|iPad|iPod\/i;\n\napp.get(&#39;\/products\/:id&#39;, (req, res) =&gt; {\n  const isMobile = mobileRe.test(req.headers[&#39;user-agent&#39;] || &#39;&#39;);\n\n  if (isMobile) {\n    \/\/ Serve a lightweight landing page with app CTAs\n    res.render(&#39;product-mobile&#39;, { productId: req.params.id });\n  } else {\n    res.render(&#39;product-desktop&#39;, { productId: req.params.id });\n  }\n});\n<\/code><\/pre>\n\n\n\n<p>Be careful with this pattern. Serving completely different content based on user agent can create SEO problems if Google&#39;s crawler (which runs as a desktop agent) sees different content than mobile users. For SEO-sensitive pages, client-side progressive enhancement is safer. Server-side redirection makes more sense for dedicated campaign landing pages or post-click flows where SEO ranking is not a concern.<\/p>\n\n\n\n<p>User agent detection is also inherently imprecise. Tablet UA strings vary, in-app browsers report themselves as their host app, and the UA string can be spoofed. Use it as a strong hint, not a definitive signal.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Landing Pages as Fallback<\/h2>\n\n\n\n<p>When a user taps a deep link and does not have the app installed, they need somewhere to go. A well-designed fallback landing page does several things at once:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>It explains what the app does and why they should install it<\/li>\n<li>It preserves the context from the original link (the product they were looking at, the promo code that was shared)<\/li>\n<li>It stores that context so deferred deep linking can restore it after install<\/li>\n<\/ul>\n\n\n\n<p>For tips on building landing pages that rank in search while also routing app users effectively, see <a href=\"https:\/\/tolinku.com\/blog\/seo-app-landing-pages\/\">SEO for app landing pages<\/a>. The context preservation is the part that requires planning. When a user lands on your fallback page, you can store the original destination (and any parameters) in a server-side session keyed by a fingerprint of device attributes. When the user installs and opens the app for the first time, your SDK sends the same fingerprint to your server, matches it, and retrieves the stored destination.<\/p>\n\n\n\n<p>This is the core mechanism behind <a href=\"https:\/\/tolinku.com\/docs\/concepts\/deep-linking\/\">deferred deep linking<\/a>. For more on how attribution works across this web-to-app boundary, see <a href=\"https:\/\/tolinku.com\/blog\/web-to-app-attribution\/\">web-to-app attribution<\/a>. The landing page is not just a dead end: it&#39;s the starting point of a chain that continues through the App Store and into the app itself.<\/p>\n\n\n\n<p>A landing page for this purpose should:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Show the specific content the user was trying to reach (load the product, the promo, or whatever the link pointed to)<\/li>\n<li>Have a clear primary CTA to the App Store or Google Play<\/li>\n<li>Have a secondary option to continue on the web (do not trap users who do not want the app)<\/li>\n<li>Pass the original link parameters to the app store URL as a campaign parameter so you can track attribution<\/li>\n<\/ol>\n\n\n\n<pre><code class=\"language-html\">&lt;!-- Example: store the deep link target before redirecting to App Store --&gt;\n&lt;a id=&quot;get-app-btn&quot; href=&quot;#&quot;&gt;Get the App&lt;\/a&gt;\n\n&lt;script&gt;\n  var deepLinkTarget = new URLSearchParams(window.location.search).get(&#39;target&#39;);\n  var appStoreUrl = &#39;https:\/\/apps.apple.com\/app\/idYOUR_APP_ID&#39;;\n  var playStoreUrl = &#39;https:\/\/play.google.com\/store\/apps\/details?id=com.yourapp&#39;;\n\n  if (deepLinkTarget) {\n    \/\/ Store target for deferred deep link retrieval\n    fetch(&#39;\/api\/store-deferred-link&#39;, {\n      method: &#39;POST&#39;,\n      headers: { &#39;Content-Type&#39;: &#39;application\/json&#39; },\n      body: JSON.stringify({ target: deepLinkTarget })\n    });\n\n    appStoreUrl += &#39;&amp;ct=deferred&amp;mt=8&#39;;\n    playStoreUrl += &#39;&amp;referrer=&#39; + encodeURIComponent(&#39;target=&#39; + deepLinkTarget);\n  }\n\n  var ua = navigator.userAgent;\n  document.getElementById(&#39;get-app-btn&#39;).href =\n    \/iPhone|iPad|iPod\/.test(ua) ? appStoreUrl : playStoreUrl;\n&lt;\/script&gt;\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Putting the Layers Together<\/h2>\n\n\n\n<p>Web-to-app deep linking is not a single technique. It&#39;s a stack of techniques, each covering a different scenario:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><p><strong>Apple smart app banner meta tag<\/strong>: Zero-effort baseline for Safari on iOS. Covers users already in Safari and gives them an obvious app install CTA.<\/p>\n\n\n\n<p><strong>Custom JavaScript banner<\/strong>: Covers Android and non-Safari browsers on iOS. Gives you control over appearance and analytics.<\/p>\n\n\n\n<p><strong><code>getInstalledRelatedApps<\/code> on Android<\/strong>: Lets you know with high confidence whether the app is installed, so you can show the right CTA without a timeout hack.<\/p>\n\n\n\n<p><strong>URI scheme attempt with timeout<\/strong>: Broad fallback for platforms where <code>getInstalledRelatedApps<\/code> is not available. Unreliable but widely deployed.<\/p>\n\n\n\n<p><strong>Universal Links \/ App Links as primary deep link format<\/strong>: For users who have the app installed and tap a link in another app or browser, the OS hands off to your app directly. These should be your canonical deep link URLs.<\/p>\n\n\n\n<p><strong>Landing pages with deferred deep link support<\/strong>: For users who do not have the app installed. The page explains your app, drives installs, and preserves context across the install gap.<\/p>\n\n\n\n<p>Platforms like <a href=\"https:\/\/tolinku.com\/features\/deep-linking\">Tolinku<\/a> handle the routing, fallback logic, and deferred deep link matching as infrastructure, so you do not have to implement and maintain each layer yourself. But understanding what is happening at each layer is essential when debugging link behavior or evaluating what a platform actually covers.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Testing Your Web-to-App Links<\/h2>\n\n\n\n<p>Testing deep links from the web is tedious because the behavior depends on browser, OS version, whether the app is installed, and the specific link type. A few practices help:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Test on real devices, not simulators. iOS simulators do not support Universal Links.<\/li>\n<li>Test with the app installed and uninstalled separately.<\/li>\n<li>Test from multiple entry points: Safari, Chrome, an email client, a social app&#39;s in-app browser (each has quirks).<\/li>\n<li>For the Apple smart app banner, verify that <code>app-argument<\/code> is being passed by logging the value in your <code>application(_:open:options:)<\/code> implementation.<\/li>\n<li>For Android App Links, use <code>adb shell am start -W -a android.intent.action.VIEW -d &quot;https:\/\/yourapp.com\/products\/42&quot;<\/code> to test intent resolution directly.<\/li>\n<\/ul>\n\n\n\n<p>The in-app browser case deserves specific attention. When users tap a link inside Instagram, TikTok, or LinkedIn, those apps open a built-in WebView rather than Safari or Chrome. Universal Links and App Links often do not fire from these in-app browsers. Your custom banner and URI scheme fallback become the primary mechanism in that context. This is one of the most common sources of reported &quot;broken deep links&quot; and it is worth testing explicitly.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Start with What Matters Most<\/h2>\n\n\n\n<p>If your users are primarily iOS and you have a native app, the Apple smart app banner meta tag is two minutes of work and reaches a large portion of your audience. Add the <code>app-argument<\/code> for deep link support and you have covered the most common case.<\/p>\n\n\n\n<p>For cross-platform coverage, a <a href=\"https:\/\/tolinku.com\/docs\/user-guide\/smart-banners\/\">custom smart banner<\/a> (either built or deployed via a platform) gets you Android users and gives you analytics on how many people actually tap through. For PWA-specific considerations, see <a href=\"https:\/\/tolinku.com\/blog\/deep-linking-progressive-web-apps\/\">deep linking for progressive web apps<\/a>.<\/p>\n\n\n\n<p>The <code>getInstalledRelatedApps<\/code> API is worth adding for Android if you want to show contextually appropriate CTAs without timeout heuristics. It only takes a few lines once your asset links file is in place.<\/p>\n\n\n\n<p>And if you run any kind of paid acquisition or sharing campaigns, a proper landing page with deferred deep linking context is the piece that turns installs from anonymous events into meaningful app opens that continue the journey the user started.<\/p>\n\n\n\n<p>The web and your app do not have to be separate worlds. Every technique here makes it more likely that a mobile web visitor ends up in the experience you actually built for them.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Connect your web and app experiences with deep linking. Learn web-to-app strategies, smart banners, and cross-platform user journeys.<\/p>\n","protected":false},"author":2,"featured_media":435,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Deep Linking for Web: Bridging Browser and App Experiences","rank_math_description":"Connect your web and app experiences with deep linking. Learn web-to-app strategies, smart banners, and cross-platform user journeys.","rank_math_focus_keyword":"deep linking for web","rank_math_canonical_url":"","rank_math_facebook_title":"","rank_math_facebook_description":"","rank_math_facebook_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-deep-linking-for-web.png","rank_math_facebook_image_id":"","rank_math_twitter_title":"","rank_math_twitter_description":"","rank_math_twitter_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-deep-linking-for-web.png","footnotes":""},"categories":[11],"tags":[20,71,42,40,41],"class_list":["post-436","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-deep-linking","tag-deep-linking","tag-javascript","tag-landing-pages","tag-smart-banners","tag-web-to-app"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/436","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/comments?post=436"}],"version-history":[{"count":3,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/436\/revisions"}],"predecessor-version":[{"id":2799,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/436\/revisions\/2799"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/435"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=436"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=436"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=436"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}