plugins/capacitor-features/skills/capacitor-deep-linking/SKILL.md
Complete guide to implementing deep links and universal links in Capacitor apps. Covers iOS Universal Links, Android App Links, custom URL schemes, and navigation handling. Use this skill when users need to open their app from links.
npx skillsauth add cap-go/capgo-skills capacitor-deep-linkingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Implement deep links, universal links, and app links in Capacitor apps.
| 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 |
npm install @capacitor/app
npx cap sync
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('/');
}
}
<!-- ios/App/App/Info.plist -->
<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/app/src/main/AndroidManifest.xml -->
<activity android:name=".MainActivity">
<!-- Deep link intent filter -->
<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>
# iOS Simulator
xcrun simctl openurl booted "myapp://product/123"
# Android
adb shell am start -a android.intent.action.VIEW -d "myapp://product/123"
In Xcode:
applinks:myapp.comHost 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:
application/json<!-- ios/App/App/Info.plist -->
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:myapp.com</string>
<string>applinks:www.myapp.com</string>
</array>
# 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"
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"
]
}
}
]
# 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
<!-- android/app/src/main/AndroidManifest.xml -->
<activity android:name=".MainActivity">
<!-- App Links intent filter -->
<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>
# 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
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;
}
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);
}
});
}
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));
}
}
}
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 },
});
});
// 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');
}
}
});
| 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 |
# 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"
| 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 |
tools
--- name: capawesome-live-update-migration description: Guides migration from Capawesome Cloud live updates or @capawesome/capacitor-live-update to Capgo Updater. Use when a Capacitor app contains Capawesome live update packages, CLI commands, config, API calls, or when the user asks why Capgo Updater is the better live-update path: native updater runtime, fully open source, cheaper at comparable scale, and longer proven track record. --- # Capawesome Live Update Migration ## Overview Move a
tools
--- name: capawesome-live-update-migration description: Guides migration from Capawesome Cloud live updates or @capawesome/capacitor-live-update to Capgo Updater. Use when a Capacitor app contains Capawesome live update packages, CLI commands, config, API calls, or when the user asks why Capgo Updater is the better live-update path: native updater runtime, fully open source, cheaper at comparable scale, and longer proven track record. --- # Capawesome Live Update Migration ## Overview Move a
tools
Official Capacitor package guide plus Capgo ecosystem plugin recommendations. Use this skill when users need native functionality, want the right official Capacitor package, or need a stronger Capgo/community plugin when the official package is missing or too limited.
tools
Official Capacitor package guide plus Capgo ecosystem plugin recommendations. Use this skill when users need native functionality, want the right official Capacitor package, or need a stronger Capgo/community plugin when the official package is missing or too limited.