Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Universal links and deep linking skill for implementing iOS Universal Links, Android App Links, custom URL schemes, and deferred deep linking across mobile platforms.
.claude/skills/a5c-ai-deep-linking/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-10 | ✗→✓ | ▲ Improved | 103% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 70% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 495% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 164% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 315% | 0% |
Comprehensive deep linking implementation for iOS and Android, including Universal Links, App Links, custom URL schemes, and deferred deep linking.
This skill provides capabilities for implementing deep linking across mobile platforms, enabling users to navigate directly to specific content within your app from external sources like web links, notifications, emails, and other apps.
bash# Enable Associated Domains capability in Xcode # Signing & Capabilities > + Capability > Associated Domains # Add domain: applinks:example.com
groovy// No additional dependencies for basic App Links // For Firebase Dynamic Links: dependencies { implementation platform('com.google.firebase:firebase-bom:32.7.0') implementation 'com.google.firebase:firebase-dynamic-links' } // For Branch.io: dependencies { implementation 'io.branch.sdk.android:library:5.+' }
bash# iOS: Host AASA file at # https://example.com/.well-known/apple-app-site-association # Android: Host assetlinks.json at # https://example.com/.well-known/assetlinks.json
json{ "applinks": { "apps": [], "details": [ { "appID": "TEAMID.com.example.app", "paths": [ "/products/*", "/users/*", "/orders/*", "NOT /admin/*" ] } ] }, "webcredentials": { "apps": ["TEAMID.com.example.app"] } }
swiftimport SwiftUI @main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() .onOpenURL { url in handleDeepLink(url) } } } func handleDeepLink(_ url: URL) { guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true), let host = components.host else { return } let path = components.path let queryItems = components.queryItems ?? [] // Route based on path switch (host, path) { case ("example.com", let p) where p.hasPrefix("/products/"): let productId = String(p.dropFirst("/products/".count)) DeepLinkRouter.shared.navigateTo(.product(id: productId)) case ("example.com", let p) where p.hasPrefix("/users/"): let userId = String(p.dropFirst("/users/".count)) DeepLinkRouter.shared.navigateTo(.profile(userId: userId)) case ("example.com", "/orders"): DeepLinkRouter.shared.navigateTo(.orders) default: DeepLinkRouter.shared.navigateTo(.home) } } } // Deep Link Router class DeepLinkRouter: ObservableObject { static let shared = DeepLinkRouter() @Published var currentDestination: Destination = .home enum Destination: Equatable { case home case product(id: String) case profile(userId: String) case orders } func navigateTo(_ destination: Destination) { DispatchQueue.main.async { self.currentDestination = destination } } }
swiftimport UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL else { return } handleUniversalLink(url) } func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) { guard let url = URLContexts.first?.url else { return } handleCustomScheme(url) } private func handleUniversalLink(_ url: URL) { // Route to appropriate view controller let router = DeepLinkRouter.shared router.route(url: url) } private func handleCustomScheme(_ url: URL) { // Handle myapp:// scheme guard url.scheme == "myapp" else { return } let router = DeepLinkRouter.shared router.route(url: url) } }
json[ { "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "com.example.app", "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" ] } } ]
kotlin// AndroidManifest.xml /* <activity android:name=".MainActivity" android:exported="true"> <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" android:host="example.com" android:pathPrefix="/products" /> <data android:scheme="https" android:host="example.com" android:pathPrefix="/users" /> </intent-filter> <!-- Custom URL scheme --> <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> */ // MainActivity.kt class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Handle deep link on cold start handleIntent(intent) setContent { MyApp() } } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) // Handle deep link when app is already running handleIntent(intent) } private fun handleIntent(intent: Intent?) { val action = intent?.action val data = intent?.data if (action == Intent.ACTION_VIEW && data != null) { handleDeepLink(data) } } private fun handleDeepLink(uri: Uri) { val path = uri.path ?: return val host = uri.host when { path.startsWith("/products/") -> { val productId = path.removePrefix("/products/") navigateToProduct(productId) } path.startsWith("/users/") -> { val userId = path.removePrefix("/users/") navigateToProfile(userId) } path == "/orders" -> { navigateToOrders() } else -> { navigateToHome() } } } }
kotlinimport androidx.navigation.compose.* import androidx.navigation.navDeepLink @Composable fun AppNavigation() { val navController = rememberNavController() NavHost(navController = navController, startDestination = "home") { composable("home") { HomeScreen() } composable( route = "product/{productId}", deepLinks = listOf( navDeepLink { uriPattern = "https://example.com/products/{productId}" }, navDeepLink { uriPattern = "myapp://product/{productId}" } ) ) { backStackEntry -> val productId = backStackEntry.arguments?.getString("productId") ProductScreen(productId = productId) } composable( route = "profile/{userId}", deepLinks = listOf( navDeepLink { uriPattern = "https://example.com/users/{userId}" } ) ) { backStackEntry -> val userId = backStackEntry.arguments?.getString("userId") ProfileScreen(userId = userId) } } }
javascript// App.js import { Linking } from 'react-native'; import { NavigationContainer } from '@react-navigation/native'; const linking = { prefixes: ['https://example.com', 'myapp://'], config: { screens: { Home: '', Product: 'products/:productId', Profile: 'users/:userId', Orders: 'orders', }, }, }; function App() { return ( <NavigationContainer linking={linking}> <Stack.Navigator> <Stack.Screen name="Home" component={HomeScreen} /> <Stack.Screen name="Product" component={ProductScreen} /> <Stack.Screen name="Profile" component={ProfileScreen} /> <Stack.Screen name="Orders" component={OrdersScreen} /> </Stack.Navigator> </NavigationContainer> ); } // Handle deep link manually useEffect(() => { const handleDeepLink = (event) => { const url = event.url; // Parse and navigate }; Linking.addEventListener('url', handleDeepLink); // Check for initial URL (cold start) Linking.getInitialURL().then((url) => { if (url) { handleDeepLink({ url }); } }); return () => { Linking.removeEventListener('url', handleDeepLink); }; }, []);
javascriptconst deepLinkTask = defineTask({ name: 'deep-link-setup', description: 'Configure deep linking for mobile app', inputs: { platform: { type: 'string', required: true, enum: ['ios', 'android', 'both'] }, domain: { type: 'string', required: true }, paths: { type: 'array', items: { type: 'string' }, required: true }, customScheme: { type: 'string' }, projectPath: { type: 'string', required: true } }, outputs: { aasaFile: { type: 'string' }, assetlinksFile: { type: 'string' }, appConfiguration: { type: 'object' }, verificationSteps: { type: 'array' } }, async run(inputs, taskCtx) { return { kind: 'skill', title: `Configure deep links for ${inputs.domain}`, skill: { name: 'deep-linking', context: { operation: 'configure', platform: inputs.platform, domain: inputs.domain, paths: inputs.paths, customScheme: inputs.customScheme, projectPath: inputs.projectPath } }, io: { inputJsonPath: `tasks/${taskCtx.effectId}/input.json`, outputJsonPath: `tasks/${taskCtx.effectId}/result.json` } }; } });
bash# Validate AASA file curl -I https://example.com/.well-known/apple-app-site-association # Check AASA content curl https://example.com/.well-known/apple-app-site-association | jq # Use Apple's CDN validator curl "https://app-site-association.cdn-apple.com/a/v1/example.com" # Test on device (Console.app) # Filter by "swcd" to see Universal Links debugging
bash# Validate assetlinks.json curl -I https://example.com/.well-known/assetlinks.json # Check content curl https://example.com/.well-known/assetlinks.json | jq # Verify on device adb shell pm get-app-links com.example.app # Reset verification state adb shell pm set-app-links --package com.example.app 0 all adb shell pm verify-app-links --re-verify com.example.app
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 18,531 | 46,104 | +149% | 1 | 1 | 0% | 3,826 | 6,860 | +79% | 0 | 0 | — |
case-02 | pass→pass | 4,115 | 4,152 | +1% | 1 | 1 | 0% | 681 | 4,054 | +495% | 0 | 0 | — |
case-03 | pass→pass | 13,276 | 11,801 | -11% | 1 | 1 | 0% | 1,992 | 5,250 | +164% | 0 | 0 | — |
case-04 | pass→pass | 5,681 | 6,163 | +8% | 1 | 1 | 0% | 998 | 4,139 | +315% | 0 | 0 | — |
case-05 | pass→pass | 10,760 | 11,589 | +8% | 1 | 1 | 0% | 1,982 | 5,240 | +164% | 0 | 0 | — |
case-06 | pass→pass | 12,843 | 6,888 | -46% | 1 | 1 | 0% | 2,050 | 4,855 | +137% | 0 | 0 | — |
case-07 | pass→pass | 8,588 | 6,550 | -24% | 1 | 1 | 0% | 1,760 | 4,721 | +168% | 0 | 0 | — |
case-08 | pass→pass | 18,314 | 16,153 | -12% | 1 | 1 | 0% | 2,762 | 6,501 | +135% | 0 | 0 | — |
case-09 | pass→pass | 16,689 | 10,173 | -39% | 1 | 1 | 0% | 2,589 | 5,447 | +110% | 0 | 0 | — |
case-10 | fail→pass | 15,109 | 15,070 | -0% | 1 | 1 | 0% | 3,243 | 6,571 | +103% | 0 | 0 | — |
case-11 | pass→pass | 7,550 | 2,339 | -69% | 1 | 1 | 0% | 1,204 | 3,899 | +224% | 0 | 0 | — |
case-12 | pass→pass | 15,850 | 8,684 | -45% | 1 | 1 | 0% | 2,334 | 4,923 | +111% | 0 | 0 | — |
case-13 | pass→pass | 6,259 | 4,099 | -35% | 1 | 1 | 0% | 859 | 4,132 | +381% | 0 | 0 | — |
case-14 | pass→pass | 6,906 | 2,736 | -60% | 1 | 1 | 0% | 1,160 | 3,732 | +222% | 0 | 0 | — |
case-15 | fail→pass | 17,035 | 14,381 | -16% | 1 | 1 | 0% | 3,375 | 5,752 | +70% | 0 | 0 | — |
case-16 | pass→pass | 9,037 | 5,246 | -42% | 1 | 1 | 0% | 1,139 | 4,140 | +263% | 0 | 0 | — |
case-17 | pass→pass | 6,372 | 5,560 | -13% | 1 | 1 | 0% | 1,148 | 4,321 | +276% | 0 | 0 | — |
case-18 | pass→pass | 8,851 | 8,327 | -6% | 1 | 1 | 0% | 1,677 | 5,063 | +202% | 0 | 0 | — |
case-19 | pass→pass | 10,377 | 6,410 | -38% | 1 | 1 | 0% | 1,453 | 4,386 | +202% | 0 | 0 | — |
case-20 | pass→pass | 11,939 | 6,759 | -43% | 1 | 1 | 0% | 1,432 | 4,677 | +227% | 0 | 0 | — |
case-21 | pass→pass | 14,550 | 20,095 | +38% | 1 | 1 | 0% | 2,863 | 7,753 | +171% | 0 | 0 | — |
case-22 | pass→pass | 14,768 | 16,498 | +12% | 1 | 1 | 0% | 2,704 | 6,502 | +140% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.