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​
- Set up your React Navigation linking config
- Add prefixes correctly
- Confirm your
app.json
/app.config.js
has ascheme
property inapps/expo
- Configure
associatedDomains
(iOS) andintentFilters
(Android) in yourapp.json
/app.config.js
inapps/expo
- Set up
apple-app-site-association
on your Next.js app.- This part is unique to this guide.
- Test it out
Android instructions for universal linking aren't done yet. I'd accept a PR though.
1. Linking config​
- React Navigation Docs
- Example linking config
- Note that this example only has one prefix. If you want Universal Links to work (meaning
yourdomain.com
should open your app), be sure to add the correct prefixes.
- Note that this example only has one prefix. If you want Universal Links to work (meaning
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 propertiesassociatedDomains: [`applinks:${url}`],},android: {// ...other android propertiesintentFilters: [{action: 'VIEW',autoVerify: true,data: [{scheme: 'https',host: `*.${url}`,pathPrefix: '/',},],category: ['BROWSABLE', 'DEFAULT'],},],},}
js
const url = 'beatgig.com'export default {ios: {// ...other ios propertiesassociatedDomains: [`applinks:${url}`],},android: {// ...other android propertiesintentFilters: [{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 IDconst TEAM_ID = 'YOUR-APPLE-APP-STORE-TEAM-ID' // replace with your Apple Team IDconst 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 IDconst TEAM_ID = 'YOUR-APPLE-APP-STORE-TEAM-ID' // replace with your Apple Team IDconst 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 pagespaths: [// 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 pagespaths: [// 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.