Skip to main content

Deep Linking

Before starting, you should reference the extensive docs from React Navigation for setting up Deep Linking:

While React Navigation's docs cover the setup for the iOS/Android app, this guide aims to be a complete reference for enabling Universal Linking, including the code needed on the website.

Video

Overview

  1. Set up your React Navigation linking config
    1. Add prefixes correctly
  2. Confirm your app.json/app.config.js has a scheme property in apps/expo
  3. Configure associatedDomains (iOS) and intentFilters (Android) in your app.json/app.config.js in apps/expo
  4. Set up apple-app-site-association on your Next.js app.
    1. This part is unique to this guide.
  5. Test it out

Android instructions for universal linking aren't done yet. I'd accept a PR though.

1. Linking config

For example, if your website is beatgig.com, you might have something like this in your prefixes:

ts
import * as Linking from 'expo-linking'
const url = 'beatgig.com'
const config = {
prefixes: [
Linking.createURL('/'),
// https, including subdomains like www.
`https://${url}/`,
`https://*.${url}/`,
// http, including subdomains like www.
`http://${url}/`,
`http://*.${url}/`,
],
// ...
}
ts
import * as Linking from 'expo-linking'
const url = 'beatgig.com'
const config = {
prefixes: [
Linking.createURL('/'),
// https, including subdomains like www.
`https://${url}/`,
`https://*.${url}/`,
// http, including subdomains like www.
`http://${url}/`,
`http://*.${url}/`,
],
// ...
}

2. Scheme

Confirm your app.json/app.config.js has a scheme property in apps/expo

js
export default {
scheme: 'solito', // replace with your app scheme
}
js
export default {
scheme: 'solito', // replace with your app scheme
}

Your app scheme will let you link into your app. For example, if your scheme is solito, then solito:/// will open the app from your iPhone.

3. associatedDomains + intentFilters

Expo docs:

Your app.json/app.config.js will need to specify the domain you want to receive links from.

For example, imagine you want the app to open from beatgig.com. Then in app.config.js:

js
const url = 'beatgig.com'
export default {
ios: {
// ...other ios properties
associatedDomains: [`applinks:${url}`],
},
android: {
// ...other android properties
intentFilters: [
{
action: 'VIEW',
autoVerify: true,
data: [
{
scheme: 'https',
host: `*.${url}`,
pathPrefix: '/',
},
],
category: ['BROWSABLE', 'DEFAULT'],
},
],
},
}
js
const url = 'beatgig.com'
export default {
ios: {
// ...other ios properties
associatedDomains: [`applinks:${url}`],
},
android: {
// ...other android properties
intentFilters: [
{
action: 'VIEW',
autoVerify: true,
data: [
{
scheme: 'https',
host: `*.${url}`,
pathPrefix: '/',
},
],
category: ['BROWSABLE', 'DEFAULT'],
},
],
},
}

Notice that ios has applinks: prefixing the URL.

4. Apple App Site association

In order to get Universal Links to work, Apple requires that your website has a file at the path /.well-known/apple-app-site-association on your URL.

1. Create an API route

Create a file at this exact location in your Solito repo:

apps/next/pages/api/.well-known/apple-app-site-association.ts

Paste the contents from the code block below. Your TEAM_ID is found on AppStore Connect when you click the icon in the top right.

Your bundle ID will look something like com.nandorojo.app. It must exactly match the ios.bundleIdentifier from your app.config.js/app.json in apps/expo.

ts
import type { NextApiRequest, NextApiResponse } from 'next'
const BUNDLE_ID = 'YOUR-APPLE-APP-BUNDLE-ID' // replace with your bundle ID
const TEAM_ID = 'YOUR-APPLE-APP-STORE-TEAM-ID' // replace with your Apple Team ID
const association = {
applinks: {
apps: [],
details: [
{
appID: `${TEAM_ID}.${BUNDLE_ID}`,
paths: [
// this makes every path open your app
// this is often not desired
// see the Apple docs to configure this with granularity
'*',
],
},
],
},
}
export default (_: NextApiRequest, response: NextApiResponse) => {
return response.status(200).send(association)
}
ts
import type { NextApiRequest, NextApiResponse } from 'next'
const BUNDLE_ID = 'YOUR-APPLE-APP-BUNDLE-ID' // replace with your bundle ID
const TEAM_ID = 'YOUR-APPLE-APP-STORE-TEAM-ID' // replace with your Apple Team ID
const association = {
applinks: {
apps: [],
details: [
{
appID: `${TEAM_ID}.${BUNDLE_ID}`,
paths: [
// this makes every path open your app
// this is often not desired
// see the Apple docs to configure this with granularity
'*',
],
},
],
},
}
export default (_: NextApiRequest, response: NextApiResponse) => {
return response.status(200).send(association)
}

To see more about how to configure your links, read Apple's docs.

By default, the above code will make any URL clicked from your phone that matches your domain open the app. To change this, you can edit the paths field. For example, you probably don't want someone clicking your marketing page and then deep linking into your app:

ts
const BUNDLE_ID = 'YOUR-APPLE-APP-BUNDLE-ID'
const TEAM_ID = 'YOUR-APPLE-APP-STORE-TEAM-ID'
const association = {
applinks: {
apps: [],
details: [
{
appID: `${TEAM_ID}.${BUNDLE_ID}`,
// all paths, except for marketing pages
paths: [
// all paths, except for marketing pages where the URL starts with /products/
// order matters! the first matched case will be used
'NOT /products/*',
'*',
],
},
],
},
}
export default (_: NextApiRequest, response: NextApiResponse) => {
return response.status(200).send(association)
}
ts
const BUNDLE_ID = 'YOUR-APPLE-APP-BUNDLE-ID'
const TEAM_ID = 'YOUR-APPLE-APP-STORE-TEAM-ID'
const association = {
applinks: {
apps: [],
details: [
{
appID: `${TEAM_ID}.${BUNDLE_ID}`,
// all paths, except for marketing pages
paths: [
// all paths, except for marketing pages where the URL starts with /products/
// order matters! the first matched case will be used
'NOT /products/*',
'*',
],
},
],
},
}
export default (_: NextApiRequest, response: NextApiResponse) => {
return response.status(200).send(association)
}

2. Add a redirect

Finally, Apple expects this file to exist at the exact path yourdomain.com/.well-known/apple-app-site-association.

Since the file we created above is an API route, we need to create a redirect in next.config.js:

apps/next/next.config.js

js
const nextConfig = {
// ...
async redirects() {
return [
{
source: '/.well-known/:file',
destination: '/api/.well-known/:file',
permanent: false,
},
]
},
}
// ...
js
const nextConfig = {
// ...
async redirects() {
return [
{
source: '/.well-known/:file',
destination: '/api/.well-known/:file',
permanent: false,
},
]
},
}
// ...

Your final next.config.js file might look like this:

js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
webpack5: true,
async redirects() {
return [
{
source: '/.well-known/:file',
destination: '/api/.well-known/:file',
permanent: false,
},
]
},
}
const { withExpo } = require('@expo/next-adapter')
const withPlugins = require('next-compose-plugins')
const withTM = require('next-transpile-modules')([
'solito',
'dripsy',
'@dripsy/core',
'moti',
'@motify/core',
'@motify/components',
'app',
])
module.exports = withPlugins(
[withTM, [withExpo, { projectRoot: __dirname }]],
nextConfig
)
js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
webpack5: true,
async redirects() {
return [
{
source: '/.well-known/:file',
destination: '/api/.well-known/:file',
permanent: false,
},
]
},
}
const { withExpo } = require('@expo/next-adapter')
const withPlugins = require('next-compose-plugins')
const withTM = require('next-transpile-modules')([
'solito',
'dripsy',
'@dripsy/core',
'moti',
'@motify/core',
'@motify/components',
'app',
])
module.exports = withPlugins(
[withTM, [withExpo, { projectRoot: __dirname }]],
nextConfig
)

5. Test your app

First, you'll need to make sure you publish your website on the domain you used.

Once it's live, create a new build of your Expo app with expo run:ios -d. Plug your iPhone into your computer to test it out. You can also build it with EAS, Expo's cloud build service.

Next, try texting yourself a URL, and see if it deep links.

It's worth noting that Apple caches your website's apple-app-site-association for a specific install of the iPhone app. As a result, if you update your Next.js site's apple-app-site-association file, it won't be reflected in the iPhone unless you reinstall it.