{"id":1866,"date":"2026-07-27T17:00:00","date_gmt":"2026-07-27T22:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=1866"},"modified":"2026-03-07T03:50:15","modified_gmt":"2026-03-07T08:50:15","slug":"capacitor-deep-linking","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/capacitor-deep-linking\/","title":{"rendered":"Capacitor Deep Linking: Setup for Ionic Apps"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Capacitor apps are web apps wrapped in a native shell. Deep linking in Capacitor requires native configuration (AASA, assetlinks.json, entitlements, Intent filters) plus JavaScript-side URL handling. This guide covers both sides.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For the cross-platform deep linking overview, see <a href=\"https:\/\/tolinku.com\/blog\/cross-platform-deep-linking-guide\/\">cross-platform deep linking guide for 2026<\/a>. For Ionic-specific configuration, see <a href=\"https:\/\/tolinku.com\/blog\/ionic-deep-linking\/\">Ionic deep linking: complete configuration guide<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Capacitor 5+ (current LTS)<\/li>\n<li>An HTTPS domain you control<\/li>\n<li>AASA file hosted at <code>\/.well-known\/apple-app-site-association<\/code><\/li>\n<li>assetlinks.json hosted at <code>\/.well-known\/assetlinks.json<\/code><\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">iOS Configuration<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">1. Add Associated Domains Entitlement<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In Xcode, open your project (the <code>ios\/App<\/code> directory):<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Select the App target.<\/li>\n<li>Go to Signing &amp; Capabilities.<\/li>\n<li>Click &quot;+ Capability&quot; and add &quot;Associated Domains.&quot;<\/li>\n<li>Add your domain: <code>applinks:yourdomain.com<\/code><\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">This creates or updates the <code>.entitlements<\/code> file:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;key&gt;com.apple.developer.associated-domains&lt;\/key&gt;\n&lt;array&gt;\n    &lt;string&gt;applinks:yourdomain.com&lt;\/string&gt;\n&lt;\/array&gt;\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">2. AASA File<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Host at <code>https:\/\/yourdomain.com\/.well-known\/apple-app-site-association<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-json\">{\n  &quot;applinks&quot;: {\n    &quot;details&quot;: [\n      {\n        &quot;appIDs&quot;: [&quot;TEAMID.com.yourcompany.yourapp&quot;],\n        &quot;components&quot;: [\n          { &quot;\/&quot;: &quot;\/products\/*&quot; },\n          { &quot;\/&quot;: &quot;\/offers\/*&quot; },\n          { &quot;\/&quot;: &quot;\/referral\/*&quot; }\n        ]\n      }\n    ]\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">3. Handle the URL in AppDelegate<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Capacitor&#39;s default <code>AppDelegate.swift<\/code> already includes the Universal Links handler. Verify it exists:<\/p>\n\n\n\n<pre><code class=\"language-swift\">func application(_ application: UIApplication,\n                 continue userActivity: NSUserActivity,\n                 restorationHandler: @escaping ([UIUserActivityRestoring]?) -&gt; Void) -&gt; Bool {\n    return CAPBridge.handleContinueActivity(userActivity, restorationHandler: restorationHandler)\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This forwards Universal Link URLs to Capacitor&#39;s bridge, which makes them available to your JavaScript code.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Android Configuration<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">1. Intent Filters<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Edit <code>android\/app\/src\/main\/AndroidManifest.xml<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;activity\n    android:name=&quot;com.yourcompany.yourapp.MainActivity&quot;\n    android:exported=&quot;true&quot;&gt;\n\n    &lt;!-- Existing intent filters... --&gt;\n\n    &lt;!-- App Links --&gt;\n    &lt;intent-filter android:autoVerify=&quot;true&quot;&gt;\n        &lt;action android:name=&quot;android.intent.action.VIEW&quot; \/&gt;\n        &lt;category android:name=&quot;android.intent.category.DEFAULT&quot; \/&gt;\n        &lt;category android:name=&quot;android.intent.category.BROWSABLE&quot; \/&gt;\n        &lt;data android:scheme=&quot;https&quot;\n              android:host=&quot;yourdomain.com&quot;\n              android:pathPrefix=&quot;\/products&quot; \/&gt;\n        &lt;data android:scheme=&quot;https&quot;\n              android:host=&quot;yourdomain.com&quot;\n              android:pathPrefix=&quot;\/offers&quot; \/&gt;\n        &lt;data android:scheme=&quot;https&quot;\n              android:host=&quot;yourdomain.com&quot;\n              android:pathPrefix=&quot;\/referral&quot; \/&gt;\n    &lt;\/intent-filter&gt;\n&lt;\/activity&gt;\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">2. assetlinks.json<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Host at <code>https:\/\/yourdomain.com\/.well-known\/assetlinks.json<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-json\">[\n  {\n    &quot;relation&quot;: [&quot;delegate_permission\/common.handle_all_urls&quot;],\n    &quot;target&quot;: {\n      &quot;namespace&quot;: &quot;android_app&quot;,\n      &quot;package_name&quot;: &quot;com.yourcompany.yourapp&quot;,\n      &quot;sha256_cert_fingerprints&quot;: [\n        &quot;YOUR_SIGNING_KEY_FINGERPRINT&quot;\n      ]\n    }\n  }\n]\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Get your fingerprint:<\/p>\n\n\n\n<pre><code class=\"language-bash\"># Debug key\nkeytool -list -v -keystore ~\/.android\/debug.keystore -alias androiddebugkey -storepass android\n\n# Release key (Play App Signing)\n# Find in Google Play Console &gt; Setup &gt; App signing\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">JavaScript-Side Handling<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Using the App Plugin<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Capacitor&#39;s built-in <code>@capacitor\/app<\/code> plugin provides the <code>appUrlOpen<\/code> event:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">import { App } from &#39;@capacitor\/app&#39;;\nimport { Router } from &#39;@angular\/router&#39;; \/\/ or your framework&#39;s router\n\n\/\/ Listen for deep link opens\nApp.addListener(&#39;appUrlOpen&#39;, (event) =&gt; {\n  console.log(&#39;Deep link URL:&#39;, event.url);\n\n  const url = new URL(event.url);\n  const path = url.pathname;\n  const params = Object.fromEntries(url.searchParams);\n\n  \/\/ Route to the correct page\n  handleDeepLink(path, params);\n});\n\nfunction handleDeepLink(path: string, params: Record&lt;string, string&gt;) {\n  \/\/ Match routes\n  const productMatch = path.match(\/^\\\/products\\\/([^\/]+)$\/);\n  if (productMatch) {\n    router.navigate([&#39;\/product&#39;, productMatch[1]], { queryParams: params });\n    return;\n  }\n\n  const offerMatch = path.match(\/^\\\/offers\\\/([^\/]+)$\/);\n  if (offerMatch) {\n    router.navigate([&#39;\/offer&#39;, offerMatch[1]], { queryParams: params });\n    return;\n  }\n\n  const referralMatch = path.match(\/^\\\/referral\\\/([^\/]+)$\/);\n  if (referralMatch) {\n    router.navigate([&#39;\/referral&#39;, referralMatch[1]], { queryParams: params });\n    return;\n  }\n\n  \/\/ Default: go home\n  router.navigate([&#39;\/&#39;]);\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Handling Cold Start vs Warm Start<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Deep links can arrive when the app is:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Cold start:<\/strong> App is not running. The URL is available when the app initializes.<\/li>\n<li><strong>Warm start:<\/strong> App is in the background. The <code>appUrlOpen<\/code> event fires.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Handle the cold start case by checking for a URL on app initialization:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">import { App } from &#39;@capacitor\/app&#39;;\n\nasync function initializeApp() {\n  \/\/ Check if the app was launched from a deep link\n  const launchUrl = await App.getLaunchUrl();\n  if (launchUrl?.url) {\n    console.log(&#39;App launched from deep link:&#39;, launchUrl.url);\n    handleDeepLink(new URL(launchUrl.url).pathname, {});\n  }\n\n  \/\/ Listen for subsequent deep links (warm start)\n  App.addListener(&#39;appUrlOpen&#39;, (event) =&gt; {\n    handleDeepLink(new URL(event.url).pathname, {});\n  });\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">With Angular Router<\/h3>\n\n\n\n<pre><code class=\"language-typescript\">import { Injectable } from &#39;@angular\/core&#39;;\nimport { Router } from &#39;@angular\/router&#39;;\nimport { App } from &#39;@capacitor\/app&#39;;\n\n@Injectable({ providedIn: &#39;root&#39; })\nexport class DeepLinkService {\n  constructor(private router: Router) {}\n\n  initialize() {\n    \/\/ Cold start\n    App.getLaunchUrl().then(result =&gt; {\n      if (result?.url) this.route(result.url);\n    });\n\n    \/\/ Warm start\n    App.addListener(&#39;appUrlOpen&#39;, event =&gt; {\n      this.route(event.url);\n    });\n  }\n\n  private route(urlString: string) {\n    const url = new URL(urlString);\n    \/\/ Angular router can handle the path directly\n    this.router.navigateByUrl(url.pathname + url.search);\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">With React (Capacitor + React)<\/h3>\n\n\n\n<pre><code class=\"language-tsx\">import { useEffect } from &#39;react&#39;;\nimport { useNavigate } from &#39;react-router-dom&#39;;\nimport { App } from &#39;@capacitor\/app&#39;;\n\nfunction useDeepLinks() {\n  const navigate = useNavigate();\n\n  useEffect(() =&gt; {\n    \/\/ Cold start\n    App.getLaunchUrl().then(result =&gt; {\n      if (result?.url) {\n        const url = new URL(result.url);\n        navigate(url.pathname + url.search);\n      }\n    });\n\n    \/\/ Warm start\n    const listener = App.addListener(&#39;appUrlOpen&#39;, event =&gt; {\n      const url = new URL(event.url);\n      navigate(url.pathname + url.search);\n    });\n\n    return () =&gt; {\n      listener.then(l =&gt; l.remove());\n    };\n  }, [navigate]);\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Testing<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">iOS<\/h3>\n\n\n\n<pre><code class=\"language-bash\"># Test on iOS Simulator\nxcrun simctl openurl booted &quot;https:\/\/yourdomain.com\/products\/123&quot;\n\n# If the app opens and navigates to the product page, it works\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Android<\/h3>\n\n\n\n<pre><code class=\"language-bash\"># Test on connected device\/emulator\nadb shell am start -a android.intent.action.VIEW \\\n  -d &quot;https:\/\/yourdomain.com\/products\/123&quot; \\\n  -c android.intent.category.BROWSABLE\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">In the Browser (Development)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">During development, you can test routing logic in the browser by navigating directly to the path:<\/p>\n\n\n\n<pre><code>http:\/\/localhost:8100\/products\/123\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This tests the routing logic but not the native deep link handling.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Common Issues<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Issue<\/th>\n<th>Cause<\/th>\n<th>Fix<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td><code>appUrlOpen<\/code> not firing<\/td>\n<td>Missing Associated Domains entitlement<\/td>\n<td>Add <code>applinks:yourdomain.com<\/code> in Xcode<\/td>\n<\/tr>\n<tr>\n<td>App opens but home screen shows<\/td>\n<td>Route handler not matching the path<\/td>\n<td>Debug the URL parsing logic<\/td>\n<\/tr>\n<tr>\n<td>Works on Android but not iOS<\/td>\n<td>AASA file issue<\/td>\n<td>Check AASA with <code>curl<\/code> and Apple&#39;s validator<\/td>\n<\/tr>\n<tr>\n<td>Works from Notes but not email<\/td>\n<td>Email client wraps the link<\/td>\n<td>Use a custom tracking domain<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Tolinku for Capacitor Apps<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/tolinku.com\/docs\/developer\/sdks\/web\/\">Tolinku&#39;s web SDK<\/a> works with Capacitor apps for smart banners and analytics. <a href=\"https:\/\/tolinku.com\/features\/deep-linking\">Tolinku<\/a> hosts your AASA and assetlinks.json files automatically.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For web-to-app deep linking, see <a href=\"https:\/\/tolinku.com\/blog\/deep-linking-for-web\/\">deep linking for web: bridging browser and app experiences<\/a>. For the cross-platform guide, see <a href=\"https:\/\/tolinku.com\/blog\/cross-platform-deep-linking-guide\/\">cross-platform deep linking guide for 2026<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Add deep linking to Capacitor and Ionic apps. Configure Universal Links, App Links, and handle deep link navigation in your web-native app.<\/p>\n","protected":false},"author":2,"featured_media":1865,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Capacitor Deep Linking: Setup for Ionic Apps","rank_math_description":"Add deep linking to Capacitor and Ionic apps. Configure Universal Links, App Links, and handle deep link navigation in your web-native app.","rank_math_focus_keyword":"Capacitor deep linking","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-capacitor-deep-linking.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-capacitor-deep-linking.png","footnotes":""},"categories":[15],"tags":[23,187,156,20,563,69,290,22],"class_list":["post-1866","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-engineering","tag-app-links","tag-capacitor","tag-cross-platform","tag-deep-linking","tag-ionic","tag-mobile-development","tag-typescript","tag-universal-links"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1866","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=1866"}],"version-history":[{"count":1,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1866\/revisions"}],"predecessor-version":[{"id":1867,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1866\/revisions\/1867"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/1865"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=1866"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=1866"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=1866"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}