Deep Linking in Capacitor
Implement deep links, universal links, and app links in Capacitor apps.
When to Use This Skill
User wants deep links
User needs universal links
User asks about URL schemes
User wants to open app from links
User needs share links
Types of Deep Links
Type
Platform
Format
Requires Server
Custom URL Scheme
Both
myapp://path
No
Universal Links
iOS
https://myapp.com/path
Yes
App Links
Android
https://myapp.com/path
Yes
Quick Start
Install Plugin
bun
add
@capacitor/app
bunx cap
sync
Handle Deep Links
import
{
App
}
from
'@capacitor/app'
;
// Listen for deep link opens
App
.
addListener
(
'appUrlOpen'
,
(
event
)
=>
{
console
.
log
(
'App opened with URL:'
,
event
.
url
)
;
// Parse and navigate
const
url
=
new
URL
(
event
.
url
)
;
handleDeepLink
(
url
)
;
}
)
;
function
handleDeepLink
(
url
:
URL
)
{
// Custom scheme: myapp://product/123
// Universal link: https://myapp.com/product/123
const
path
=
url
.
pathname
||
url
.
host
+
url
.
pathname
;
// Route based on path
if
(
path
.
startsWith
(
'/product/'
)
)
{
const
productId
=
path
.
split
(
'/'
)
[
2
]
;
navigateTo
(
/product/
${
productId
}
)
;
}
else
if
(
path
.
startsWith
(
'/user/'
)
)
{
const
userId
=
path
.
split
(
'/'
)
[
2
]
;
navigateTo
(
/profile/
${
userId
}
)
;
}
else
if
(
path
===
'/login'
)
{
navigateTo
(
'/login'
)
;
}
else
{
navigateTo
(
'/'
)
;
}
}
Custom URL Scheme
iOS Configuration
< key
CFBundleURLTypes </ key
< array
< dict
< key
CFBundleURLName </ key
< string
com.yourcompany.yourapp </ string
< key
CFBundleURLSchemes </ key
< array
< string
myapp </ string
< string
myapp-dev </ string
</ array
</ dict
</ array
Android Configuration
< activity android: name = " .MainActivity "
< intent-filter
< action android: name = " android.intent.action.VIEW " /> < category android: name = " android.intent.category.DEFAULT " /> < category android: name = " android.intent.category.BROWSABLE " /> < data android: scheme = " myapp " /> </ intent-filter
</ activity
Test Custom Scheme
iOS Simulator
xcrun simctl openurl booted "myapp://product/123"
Android
adb shell am start -a android.intent.action.VIEW -d "myapp://product/123" Universal Links (iOS) 1. Enable Associated Domains In Xcode: Select App target Signing & Capabilities Capability > Associated Domains Add: applinks:myapp.com 2. Create apple-app-site-association Host at https://myapp.com/.well-known/apple-app-site-association : { "applinks" : { "apps" : [ ] , "details" : [ { "appID" : "TEAMID.com.yourcompany.yourapp" , "paths" : [ "/product/" , "/user/" , "/invite/" , "NOT /api/" ] } ] } } Requirements : Served over HTTPS Content-Type: application/json No redirects File at root domain 3. Info.plist
< key
com.apple.developer.associated-domains </ key
< array
< string
applinks:myapp.com </ string
< string
applinks:www.myapp.com </ string
</ array
Verify Universal Links
Validate AASA file
curl -I https://myapp.com/.well-known/apple-app-site-association
Check Apple CDN cache
curl "https://app-site-association.cdn-apple.com/a/v1/myapp.com" App Links (Android) 1. Create assetlinks.json Host at https://myapp.com/.well-known/assetlinks.json : [ { "relation" : [ "delegate_permission/common.handle_all_urls" ] , "target" : { "namespace" : "android_app" , "package_name" : "com.yourcompany.yourapp" , "sha256_cert_fingerprints" : [ "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99" ] } } ] Get SHA256 Fingerprint
Debug keystore
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
Release keystore
keytool -list -v -keystore release.keystore -alias your-alias
From APK
keytool -printcert -jarfile app-release.apk 2. AndroidManifest.xml
< activity android: name = " .MainActivity "
< intent-filter android: autoVerify = " true "
< action android: name = " android.intent.action.VIEW " /> < category android: name = " android.intent.category.DEFAULT " /> < category android: name = " android.intent.category.BROWSABLE " /> < data android: scheme = " https " /> < data android: host = " myapp.com " /> < data android: pathPrefix = " /product " /> < data android: pathPrefix = " /user " /> < data android: pathPrefix = " /invite " /> </ intent-filter
</ activity
Verify App Links
Validate assetlinks.json
curl https://myapp.com/.well-known/assetlinks.json
Use Google's validator
https://developers.google.com/digital-asset-links/tools/generator
Check link handling on device
adb shell pm get-app-links com.yourcompany.yourapp Advanced Routing React Router Integration import { App } from '@capacitor/app' ; import { useHistory } from 'react-router-dom' ; import { useEffect } from 'react' ; function DeepLinkHandler ( ) { const history = useHistory ( ) ; useEffect ( ( ) => { App . addListener ( 'appUrlOpen' , ( event ) => { const url = new URL ( event . url ) ; const path = getPathFromUrl ( url ) ; // Navigate using React Router history . push ( path ) ; } ) ; // Check if app was opened with URL App . getLaunchUrl ( ) . then ( ( result ) => { if ( result ?. url ) { const url = new URL ( result . url ) ; const path = getPathFromUrl ( url ) ; history . push ( path ) ; } } ) ; } , [ ] ) ; return null ; } function getPathFromUrl ( url : URL ) : string { // Handle both custom scheme and https if ( url . protocol === 'myapp:' ) { return '/' + url . host + url . pathname ; } return url . pathname + url . search ; } Vue Router Integration import { App } from '@capacitor/app' ; import { useRouter } from 'vue-router' ; import { onMounted } from 'vue' ; export function useDeepLinks ( ) { const router = useRouter ( ) ; onMounted ( async ( ) => { App . addListener ( 'appUrlOpen' , ( event ) => { const path = parseDeepLink ( event . url ) ; router . push ( path ) ; } ) ; const launchUrl = await App . getLaunchUrl ( ) ; if ( launchUrl ?. url ) { const path = parseDeepLink ( launchUrl . url ) ; router . push ( path ) ; } } ) ; } Deferred Deep Links Handle links when app wasn't installed: import { App } from '@capacitor/app' ; import { Preferences } from '@capacitor/preferences' ; // On first launch, check for deferred link async function checkDeferredDeepLink ( ) { const { value : isFirstLaunch } = await Preferences . get ( { key : 'firstLaunch' } ) ; if ( isFirstLaunch !== 'false' ) { await Preferences . set ( { key : 'firstLaunch' , value : 'false' } ) ; // Check with your attribution service const deferredLink = await fetchDeferredLink ( ) ; if ( deferredLink ) { handleDeepLink ( new URL ( deferredLink ) ) ; } } } Query Parameters App . addListener ( 'appUrlOpen' , ( event ) => { const url = new URL ( event . url ) ; // Get query parameters const source = url . searchParams . get ( 'source' ) ; const campaign = url . searchParams . get ( 'campaign' ) ; const referrer = url . searchParams . get ( 'ref' ) ; // Track attribution analytics . logEvent ( 'deep_link_open' , { path : url . pathname , source , campaign , referrer , } ) ; // Navigate with state navigateTo ( url . pathname , { state : { source , campaign , referrer } , } ) ; } ) ; OAuth Callback Handling // Handle OAuth redirect App . addListener ( 'appUrlOpen' , async ( event ) => { const url = new URL ( event . url ) ; if ( url . pathname === '/oauth/callback' ) { const code = url . searchParams . get ( 'code' ) ; const state = url . searchParams . get ( 'state' ) ; const error = url . searchParams . get ( 'error' ) ; if ( error ) { handleOAuthError ( error ) ; return ; } if ( code && validateState ( state ) ) { await exchangeCodeForToken ( code ) ; navigateTo ( '/home' ) ; } } } ) ; Testing Test Matrix Scenario Command Custom scheme myapp://path Universal link cold start Tap link with app closed Universal link warm start Tap link with app in background Universal link in Safari Type URL in Safari App link cold start Tap link with app closed App link in Chrome Tap link in Chrome Debug Tools
iOS: Check associated domains entitlement
codesign -d --entitlements - App.app | grep associated-domains
iOS: Reset Universal Links cache
xcrun simctl erase all
Android: Check verified links
adb shell dumpsys package d | grep -A5 "Package: com.yourcompany.yourapp" Common Issues Issue Solution Universal Links not working Check AASA file, SSL, entitlements App Links not verified Check assetlinks.json, fingerprint Links open in browser Check intent-filter, autoVerify Cold start not handled Use App.getLaunchUrl() Simulator issues Reset simulator, rebuild app Resources Capacitor App Plugin: https://capacitorjs.com/docs/apis/app Universal Links Guide: https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app Android App Links: https://developer.android.com/training/app-links Digital Asset Links Validator: https://developers.google.com/digital-asset-links/tools/generator