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.jshas aschemeproperty inapps/expo - Configure
associatedDomains(iOS) andintentFilters(Android) in yourapp.json/app.config.jsinapps/expo - Set up
apple-app-site-associationon 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.comshould 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:
tsimport * 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}/`,],// ...}
tsimport * 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
jsexport default {scheme: 'solito', // replace with your app scheme}
jsexport 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:
jsconst 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'],},],},}
jsconst 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.
tsimport 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)}
tsimport 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:
tsconst 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)}
tsconst 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
jsconst nextConfig = {// ...async redirects() {return [{source: '/.well-known/:file',destination: '/api/.well-known/:file',permanent: false,},]},}// ...
jsconst 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','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','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.