Deep Links

Linking and Deep Links in Expo Router

Because every screen already has a URL, deep links need no routing code. Handling App Links in BookNest parsed booknest://book/3 by hand; with Expo Router 6,418 , the scheme in app.json and the App Links intent filter from Universal Links and App Links are enough. The router reads the incoming URL, drops the scheme, and navigates to the matching file. Sending three links to the emulator, the first while the app was stopped:

Opening BookNest from links with adbShell
adb shell am start -W -a android.intent.action.VIEW \
  -d "booknest://book/3" com.anonymous.booknest
adb shell am start -W -a android.intent.action.VIEW \
  -d "https://booknest.example.com/book/5" com.anonymous.booknest
adb shell am start -W -a android.intent.action.VIEW \
  -d "booknest://shelf/9" com.anonymous.booknest
Output
Starting: Intent { act=android.intent.action.VIEW dat=booknest://book/... pkg=com.anonymous.boo
knest }
Status: ok
LaunchState: COLD
Activity: com.anonymous.booknest/.MainActivity
TotalTime: 3618
...
 LOG  incoming link: booknest://book/3 (cold start)
 LOG  path: /book/3
 LOG  incoming link: https://booknest.example.com/book/5 (running)
 LOG  path: /book/5
 LOG  incoming link: booknest://shelf/9 (running)
 LOG  path: /shelf/9

The cold start took 3.6 seconds in this debug build, which loads its JavaScript from Metro. The other two reached the running activity (the template sets launchMode="singleTask"), and the router pushed each onto the stack. /shelf/9 matches no file, so +not-found.tsx rendered "No page at /shelf/9". The incoming link lines come from a hook that sees every link first. When links arrive in a shape your files don't match, such as old share links or tracking URLs, rewrite them in src/app/+native-intent.tsx:

src/app/+native-intent.tsx: mapping old links to current routesTSX
// Runs before the router sees an incoming link. Older BookNest links looked like
// booknest://item?id=4; send them to /book/4 and pass everything else through.
export function redirectSystemPath({ path, initial }: { path: string; initial: boolean }) {
  console.log('incoming link:', path, initial ? '(cold start)' : '(running)');
  const old = /item\?id=(\d+)/.exec(path);
  return old ? `/book/${old[1]}` : path;
}

Sending booknest://item?id=4 the same way logged:

Output of 149
 LOG  incoming link: booknest://item?id=4 (running)
 LOG  path: /book/4

Inside the app, <Link href="/book/3"> and router.push() navigate, and Linking.createURL() from expo-linking builds a shareable URL. Test every link on a cold start and on a running app; initial tells the rewrite which one it is handling.