src/skills/mobile-deep-linking-app-links/SKILL.md
Deep linking patterns - Universal Links (iOS), App Links (Android), URI schemes, expo-linking API, React Navigation linking config, Expo Router automatic linking, AASA/assetlinks.json setup, deferred deep links, testing
npx skillsauth add agents-inc/skills mobile-deep-linking-app-linksInstall 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.
Quick Guide: Universal Links (iOS) and App Links (Android) are the gold standard -- they use HTTPS URLs that open your app directly or fall back to the website. Custom URI schemes (
myapp://) are simpler but less reliable (no fallback, can be hijacked). Useexpo-linkingfor URL handling (useURL,createURL,parse). Expo Router handles deep linking automatically. React Navigation requires alinkingconfig. Always test on real devices -- simulators miss edge cases.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use Universal Links (iOS) and App Links (Android) for production apps -- custom URI schemes have no fallback and can be hijacked by other apps)
(You MUST host AASA and assetlinks.json over HTTPS at /.well-known/ -- Apple and Google will reject HTTP or incorrectly hosted files)
(You MUST handle all three app states: cold start (app not running), background (app suspended), and foreground (app active) -- missing any state causes dropped links)
(You MUST test deep links on real devices -- simulators and emulators do not fully replicate OS-level link handling behavior)
(You MUST never pass sensitive data (tokens, passwords) in deep link URLs -- URLs are logged, cached, and visible in browser history)
</critical_requirements>
Auto-detection: deep link, deep linking, universal link, app link, URI scheme, custom scheme, expo-linking, Linking.useURL, Linking.createURL, Linking.parse, Linking.openURL, Linking.getInitialURL, linking config, apple-app-site-association, AASA, assetlinks.json, intentFilters, associatedDomains, deferred deep link, App Clip, Instant App, getInitialURL, addEventListener url
When to use:
adb, xcrun simctl, uri-scheme)Key patterns covered:
useURL, createURL, parse, getInitialURLlinking config with path mapping, parameter parsing, nested navigatorsWhen NOT to use:
Detailed Resources:
Deep linking connects the outside world to specific screens in your app. The goal is a seamless experience: user taps a link, your app opens to the right content. Three link types exist, each with different trade-offs:
Universal Links (iOS) / App Links (Android) -- HTTPS URLs verified by the OS. App opens directly without disambiguation dialog. Falls back to website if app not installed. Use these for production.
Custom URI schemes (myapp://path) -- Simple to set up but unreliable: no fallback if app not installed, any app can register the same scheme (hijacking risk), and some platforms block them. Use for development or internal tools only.
Deferred deep links -- User clicks link, gets sent to app store, installs app, then lands on the intended content. Requires a third-party service or custom server-side logic. Use when acquisition funnels matter.
Key architectural principle: The link handler is the entry point to your navigation. It must work in all three app states (cold start, background, foreground) and gracefully handle invalid or expired links. Never trust link parameters -- validate and sanitize them before navigating.
Expo Router vs React Navigation:
linking config object that maps URL paths to screen namesConfigure a custom scheme in your app config so links like myapp://profile/123 open your app.
{
"expo": {
"scheme": "myapp"
}
}
After adding a scheme, rebuild the app -- scheme changes require a new native build.
Why good: Simple to configure, works immediately in development, no server-side setup needed
Gotcha: Custom schemes have no fallback -- if the app is not installed, the link fails silently. Any app can register the same scheme, so there is no guarantee your app handles it.
See examples/core.md for handling incoming scheme URLs.
Universal Links require a two-way association: your server hosts an AASA file declaring which paths belong to your app, and your app declares the associated domain.
{
"expo": {
"ios": {
"associatedDomains": ["applinks:example.com"]
}
}
}
Critical: Omit the https:// protocol from the domain value. The AASA file must be served over HTTPS at https://example.com/.well-known/apple-app-site-association.
See examples/verification-files.md for the complete AASA file format and hosting requirements.
App Links use intent filters with autoVerify: true and an assetlinks.json file on your server.
{
"expo": {
"android": {
"intentFilters": [
{
"action": "VIEW",
"autoVerify": true,
"data": [
{
"scheme": "https",
"host": "example.com",
"pathPrefix": "/product"
}
],
"category": ["BROWSABLE", "DEFAULT"]
}
]
}
}
}
Critical: autoVerify: true is required -- without it, Android treats these as regular deep links (shows disambiguation dialog instead of opening directly).
See examples/verification-files.md for the assetlinks.json file format and SHA-256 fingerprint retrieval.
Use the useURL hook to handle URLs in all app states (cold start, background, foreground). Parse URLs with Linking.parse() to extract path and query parameters.
import * as Linking from "expo-linking";
export function DeepLinkHandler() {
const url = Linking.useURL();
useEffect(() => {
if (url) {
const { hostname, path, queryParams } = Linking.parse(url);
// Navigate based on parsed URL
}
}, [url]);
return null;
}
Why good: useURL handles both the initial launch URL and subsequent foreground URLs -- no need to manage getInitialURL and addEventListener separately.
See examples/core.md for createURL, parse, and complete handling patterns.
Map URL paths to screens using the linking config. Paths support parameters, optional segments, regex patterns, and nested navigators.
import * as Linking from "expo-linking";
const linking = {
prefixes: [Linking.createURL("/"), "https://example.com", "myapp://"],
config: {
screens: {
Home: "",
Profile: "user/:id",
Product: {
path: "product/:slug",
parse: { slug: (slug: string) => slug.toLowerCase() },
},
NotFound: "*",
},
},
};
Why good: Declarative path-to-screen mapping, parameter parsing built in, wildcard catch-all for unmatched URLs
See examples/core.md for nested navigator config, parameter parsing, and static API setup.
With Expo Router, every file in the app/ directory is automatically a deep linkable route. No linking config needed.
app/
index.tsx -> /
profile/[id].tsx -> /profile/123
product/[slug].tsx -> /product/blue-shirt
settings.tsx -> /settings
Why good: Zero configuration, file paths ARE the deep link paths, adding a screen automatically creates a deep link
When to use: Expo Router projects. If using React Navigation directly, use Pattern 5 instead.
Deferred deep links work when the app is not yet installed: user taps link, goes to app store, installs, then opens to the intended content. This requires server-side logic to persist the link destination through the install flow.
Implementation approaches:
Key limitation: Deferred deep links are inherently probabilistic on iOS. Android's install referrer provides deterministic matching.
See examples/core.md for deferred deep link handling patterns.
</patterns><decision_framework>
Is the app already installed on the target device?
+-- Unknown/Maybe -> Use Universal Links / App Links (HTTPS)
| +-- Needs fallback to website? -> YES, this is why HTTPS links are preferred
| +-- Needs app store redirect? -> Implement deferred deep linking
+-- YES (guaranteed, e.g. internal tool) -> Custom URI scheme is acceptable
+-- NO (acquisition funnel) -> Deferred deep link via attribution service
Do you need the OS to open your app without a disambiguation dialog?
+-- YES -> Universal Links (iOS) / App Links (Android) with verified domains
+-- NO -> Custom URI scheme (shows "Open with..." on some devices)
Which router are you using?
+-- Expo Router -> Automatic. No configuration needed. File paths = deep links.
+-- React Navigation (static API) -> Add `linking` property per screen definition
+-- React Navigation (dynamic API) -> Pass `linking` prop to NavigationContainer
+-- Custom navigation -> Use expo-linking useURL hook + manual navigation logic
| Feature | Custom URI Scheme | Universal Links (iOS) | App Links (Android) |
| --------------------- | --------------------- | --------------------- | ------------------------- |
| Format | myapp://path | https://domain/path | https://domain/path |
| Fallback | None (fails silently) | Opens website | Opens website |
| Verification | None | AASA file on server | assetlinks.json on server |
| Hijack risk | Any app can register | OS-verified, secure | OS-verified, secure |
| Setup complexity | Low | Medium | Medium |
| Works without install | No | Yes (opens website) | Yes (opens website) |
| Disambiguation dialog | Sometimes | Never (verified) | Never (verified) |
</decision_framework>
<red_flags>
High Priority Issues:
autoVerify: true on Android intent filters -- without it, App Links behave as regular deep links (disambiguation dialog shown)useURL handles this, but manual implementations that only use addEventListener will miss the launch URLMedium Priority Issues:
https:// prefix in React Navigation linking prefixes array -- Universal Links/App Links will not be matchedinitialRouteName in nested navigator linking config -- back navigation will not work correctly from deep-linked screensGotchas & Edge Cases:
example.com pointing to example.com/product/123 will NOT open the appLinking.parse() handles non-standard URL formats (like Expo Go URLs with -- separators) -- use it instead of new URL() for consistencyexp:// scheme with a different URL format (exp://127.0.0.1:8081/--/path) -- test with development builds for production-accurate behavior*) do not match / or . characters -- use multiple path entries if needed</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md
(You MUST use Universal Links (iOS) and App Links (Android) for production apps -- custom URI schemes have no fallback and can be hijacked by other apps)
(You MUST host AASA and assetlinks.json over HTTPS at /.well-known/ -- Apple and Google will reject HTTP or incorrectly hosted files)
(You MUST handle all three app states: cold start (app not running), background (app suspended), and foreground (app active) -- missing any state causes dropped links)
(You MUST test deep links on real devices -- simulators and emulators do not fully replicate OS-level link handling behavior)
(You MUST never pass sensitive data (tokens, passwords) in deep link URLs -- URLs are logged, cached, and visible in browser history)
Failure to follow these rules will result in broken deep links, security vulnerabilities, and poor user experience when links fail silently.
</critical_reminders>
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Mapbox GL JS interactive maps - map initialization, markers, popups, sources, layers, expressions, clustering, 3D terrain, geocoding, directions
tools
Leaflet interactive maps - map setup, tile layers, markers, popups, GeoJSON, custom controls, plugins, clustering, events