Branch Deep Linking Explained: Best Practices for Routing Users

Deep links take users to a specific location within a downloaded app, so they don’t have to manually search for desired content. By directing users to in-app content whenever they click a link, deep links remove friction from the user journey and enable brands to personalize user experiences, optimize app conversions, and improve app retention and engagement rates. Branch provides powerful deep links that work across different devices and platforms, including mobile web, email, and offline channels. By leveraging the tools provided by the Branch SDK and following the best practices discussed in this blog post, you can inspire customers to take action in your mobile app while collecting valuable data about your users and campaigns. 

What is the role of the Branch SDK in deep link routing?

(Note: A prerequisite to this section is having the Branch SDK installed in your app. If you don’t, please follow the steps linked here before proceeding. For a high level overview of deep links, check out the Branch deep linking guide.)

When a user clicks a Branch link to open the app, the Branch SDK will pass along any data you attached to the link. That data will be readily available for you to use anytime after the app is opened, but it is up to you to utilize it to route the user to the content of interest. We’ll cover how to do this step-by-step in the following sections!

There are unlimited properties you can use to pass deep link data to the Branch SDK, but we recommend using one of the following unless you have a custom solution that calls for something else: $canonical_url or $deeplink_path. Which one you use depends on your mobile/web/UX needs.

If your app has parity with your website

If your website functions as a mobile web version of your app for when users don’t have your app, then your app is considered to have web-to-app parity. In this case it is best to use $canonical_url, which is a property you set in the link data that corresponds to the web link. If you enter the URL in the original URL field when creating a quick link, this value will automatically populate the $canonical_url. When the app opens, you can parse that link in the app and use it to deep link the user to the proper app content. For example, we have a site hosting various Branch monsters here: https://monster-site.github.io/shop/items.html

Screenshot of the Branch MonsterSite showing various Branch monsters on a "Shop" page.

Clicking on any of these monsters will take you to the detailed view for that monster on the web. So if we click on Astrocreep, we get navigated to its detail screen. 

Screenshot of the detailed screen for the "Astrocreep" Branch monster.

Notice that the URL has a query parameter with a key of id and a value of zero: https://monster-site.github.io/shop/item-detail.html?id=0. When we create a quick link on the Branch Dashboard, we can paste the full link into the Original URL field. 

Screenshot of a Branch Dashboard on the "Define Link" page. The box "Original Web URL" is highlighted, showing the MonsterSite website URL.

Notice that the $canonical_url value automatically gets populated within the link data for this link:

Screenshot of a Branch dashboard highlighting the $canonical_url value.

When a user clicks this Branch link, the Branch SDK will surface the $canonical_url value, and you can utilize it to route the user to Astrocreep’s detail screen in the app. For example, we are taking in the $canonical_url value from the Branch link data which is: https://monster-site.github.io/shop/item-detail.html?id=0. We start by creating a URL object (from Android’s java.net.URL package) out of the canonical URL. 

//More info on $canonical_url:
https://help.branch.io/using-branch/docs/creating-a-deep-link#
:~:text=canonicalUrl,%24canonical_url
fun deepLinkUsingCanonicalURL(canonicalURL : String) {
   val url = URL(canonicalURL)

 

After that we check the value of the path of the URL. For the example link we are using, the path would be /shop/item-detail.html. Note that depending on how your website was set up you may not need to include the “.html” file extension at the end of the path. 

when(url.path) {
   "/shop/items.html" -> {
       navigationUtils.loadShopScreen()
   }
   "/shop/item-detail.html" -> {
       val id = getIdFromQueryParams(canonicalURL)
       navigationUtils.loadShopScreen(id)
   }

 

When the code hits the appropriate case, we use logic to determine which item’s detail page to route to. From there the id is used to route the user to Astrocreep’s page in the mobile app. 

Screenshot of the Astrocreep details page on the Branch MonsterSite shown on a phone.

In short, use $canonical_url to route users to the proper in-app content whenever the user can access the same content via your app and website

If your app does not have web-to-app parity

If the content you want to route the user to is present in your app but not on your website, you will need to use the $deeplink_path property from the link data. In this example, we will look at navigating to another monster from the sample, Starbeast. 

Screenshot of the details page on the Branch MonsterSite website for the "Starbeast" monster.

When creating a quick link or journey, you will need to specify the $deeplink_path property as a key, and set the value to the relevant deep link path. Notice that, as opposed to $canonical_url which is the full URL to the content, $deeplink_path is a path to the content with values separated by forward slashes. 

Screenshot of a Branch Dashboard showing the $deeplink_path field.

In this example code, we check if the deep link path contains the substring “shop” (/shop/item-detail?id=1).

//More info on $deeplink_path: https://help.branch.io/using-branch
/docs/creating-a-deep-link
#:~:text=%24deeplink_path,path%20contained%20within
fun deepLinkUsingDeepLinkPath(deepLinkPath : String) {
   if(deepLinkPath.contains("shop")) {

 

Then, we check if the deep link path contains “item-detail.” If so, we know to load that item’s detail view directly, as is the case for our example, where the id value is “1” (/shop/item-detail?id=1).

if(deepLinkPath.contains("item-detail")) {
   val id = getIdFromQueryParams(deepLinkPath)
   navigationUtils.loadShopScreen(id.toString())
} else {
   navigationUtils.loadShopScreen()
}

 

The code loads the shop screen at the intended id, which takes us to the Starbeast details screen in the app. 

Screenshot of the details page for the Starbeast monster on the Branch MonsterSite shown on a phone screen.

In case you are wondering what the “getIdFromQueryParams” function we’ve been referencing looks like, here it is: 

fun getIdFromQueryParams(url : String) : String {
   val urlQuerySanitizer = UrlQuerySanitizer()
   urlQuerySanitizer.allowUnregisteredParamaters = true
   urlQuerySanitizer.parseUrl(url)
   val id = urlQuerySanitizer.getValue("id")
   return id
}

 

The ID is present as a query parameter, so we can use Kotlin’s UrlQuerySanitizer class to map the query param keys to the values and then obtain the id.

In short, use $deeplink_path to route users to the proper in-app content whenever the content is present on your app but not your website.

$canonical_url

$deeplink_path

A full web URL

e.g. https://monster-site.github.io/shop/item-detail.html?id=0

Use when the content is present on your app AND website (web-to-app parity)

A path of values separated by forward slashes

e.g. /shop/item-detail/1

Use when the content is present on your app but NOT your website (not parity)

Using Branch in addition to existing deep linking logic

If you have an existing deep linking solution in place, be it native logic or another third-party SDK, you may be wondering if it is possible to also use Branch. It is, and this is the situation for many customers that engage with our Professional Services team. Here’s how it works. 

Whenever the app opens, the Branch SDK gets initialized. The +clicked_branch_link property will have a value of “true” if a Branch link click opens the app. If another type of link was used to open the app, or the app was opened organically (i.e. without a link click), then +clicked_branch_link will have a value of “false” and +non_branch_link will have a value of the link used to open the app. We can use the value of +clicked_branch_link in a conditional statement to determine whether we should handle deep linking via Branch or via the existing non-Branch routing logic. 

if(linkProperties?.has("+clicked_branch_link") == true && 
linkProperties.get("+clicked_branch_link") as Boolean) {
   //A Branch link was clicked, handle deep linking via Branch
} else {
   //The app opened another way, handle non-branch deep linking
}

 

Pro-tip: If your app uses existing deep link routing logic, Branch can work alongside that, and you can check the value of +clicked_branch_link to determine whether a Branch link facilitated the app opening

App open via Branch link

Other app open

+clicked_branch_link is true

+non_branch_link is null

Route to content using the Branch link data

+clicked_branch_link is false

+non_branch_link will have the value of the link that was clicked (or be null if the app was opened organically)

Route to content using your existing deep link logic

Linking to web content using Branch links

There are times when you may want a user to be sent to the web after clicking a Branch link rather than your app. For example, you wouldn’t want an unsubscribe link in an email to take a user to your app. For use cases where a Branch link should only take users to the web, Branch provides the $web_only parameter. You can add this query parameter with a value of “true” to a link or automate the process when creating quick links by toggling the Web-Only Link checkbox. 

Under the hood, this will add a “/e” to the domain of the URL (https://branchster-web.app.link/e/ex123). On iOS, this will cause users clicking the link to automatically be taken to the web because the domain will no longer match the domain listed on the AASA file. However, if the link is in an email or if a user is on the Android platform, the app will still open by default. You will need additional code to check for that parameter and route the user to the mobile web. Check for the $web_only parameter in the link data and see if it has a value of “true.” If so, route the user to the mobile web. Else, you can proceed as usual with your deep link routing logic for Branch links. 

if(linkProperties?.has("\$web_only") == true && 
linkProperties.get("\$web_only") as Boolean) {
   //A web-only link was clicked, route the user to the web
} else {
   //Handle deep link routing from the Branch-link click
}

 

Pro-tip: It is possible to have some Branch links only route the user to mobile web content by using the $web-only parameter. You’ll need to add code to check for this parameter and route the user to the content on the mobile web if it is set to a value of true

Android web-only links

iOS web-only links

App will open first, check for the $web-only parameter and if it is present and has a value of true, route the user to the mobile web

Web-only links created from the dashboard will have a /e in the path thus causing iOS to open the web

If the link is present in an email, it is click-wrapped so it will open the app first and you will need to check for the $web-only parameter and, if it is present and has a value of true, route the user to the mobile web

Deep link routing best practices

Here is a list of best practices to keep in mind when working on your deep link routing logic using the Branch SDK:

  1. Whenever possible, use $canonical_url as the parameter for routing. This parameter automatically gets populated for Journeys banners and can also be used with Branch Universal Objects to attribute app clicks back to the web content to increase its SEO ranking. 
  2. Branch SDK deep link data can be accessed anytime during the app session. Each of our SDKs features the ability to retrieve the latest referring parameters from the most recent Branch link click, enabling you to create tailored user experiences such as deep linking after an onboarding flow. 
  3. If a deep link fails, fall back to the home screen. Don’t leave your users hanging on an infinite spinner or frozen screen. Be sure to add logic to fall back to the homescreen in case a deep link is not recognized!

Following these best practices will ensure that your users have great experiences and get taken directly to the content they want. To learn more about how you can use Branch to eliminate the frustration of broken links, reach out to our team

Full source code:


package com.monster.monsterapp

import android.widget.ImageView
import androidx.fragment.app.FragmentManager
import com.google.android.material.bottomnavigation.BottomNavigationView
import org.json.JSONObject
import java.net.URL
import android.net.UrlQuerySanitizer

class BranchDeeplinking(supportFragmentManager : FragmentManager, navView: BottomNavigationView, appLogo : ImageView) {

//the keys for these fields in the link data
val canonicalURLKey : String = "\$canonical_url"
val deepLinkPathKey : String = "\$deeplink_path"

//navigationUtils is used to load screens in the app
val navigationUtils = NavigationUtils(supportFragmentManager, navView, appLogo)

/*
Will deep link using the $canonical_url or $deeplink_path depending on which is present in the link data
• $canonical_url - the full web URL, i.e. https://monster-site.github.io/shop/item-detail.html?id=1
• $deeplink_path - the local path to the content, i.e. /profile/login
*/
fun handleDeepLinkRouting(linkProperties : JSONObject?) {
if (linkProperties == null) {
return
}

var canonicalURL = ""
var deepLinkPath = ""

if(linkProperties.has(canonicalURLKey)) {
canonicalURL = linkProperties.get(canonicalURLKey) as String
}

if(linkProperties.has(deepLinkPathKey)) {
deepLinkPath = linkProperties.get(deepLinkPathKey) as String
}

if(!canonicalURL.isEmpty()) {
deepLinkUsingCanonicalURL(canonicalURL)
} else if(!deepLinkPath.isEmpty()) {
deepLinkUsingDeepLinkPath(deepLinkPath)
} else {
//neither $canonical_url or $deeplink_path were present on the link
}
}

//More info on $canonical_url: https://help.branch.io/using-branch/docs/creating-a-deep-link#:~:text=canonicalUrl,%24canonical_url
fun deepLinkUsingCanonicalURL(canonicalURL : String) {
val url = URL(canonicalURL)

when(url.path) {
"/shop/items.html" -> {
navigationUtils.loadShopScreen()
}
"/shop/item-detail.html" -> {
val id = getIdFromQueryParams(canonicalURL)
navigationUtils.loadShopScreen(id)
}
"/profile/user.html" -> {
navigationUtils.loadProfileScreen()
}
"/profile/login.html" -> {
navigationUtils.loadProfileScreen("login")
}
"/profile/create-account.html" -> {
navigationUtils.loadProfileScreen("create-account")
}
}
}

//More info on $deeplink_path: https://help.branch.io/using-branch/docs/creating-a-deep-link#:~:text=%24deeplink_path,path%20contained%20within
fun deepLinkUsingDeepLinkPath(deepLinkPath : String) {
if(deepLinkPath.contains("shop")) {
if(deepLinkPath.contains("item-detail")) {
val id = getIdFromQueryParams(deepLinkPath)
navigationUtils.loadShopScreen(id.toString())
} else {
navigationUtils.loadShopScreen()
}
} else if(deepLinkPath.contains("profile")) {
if(deepLinkPath.contains("login")) {
navigationUtils.loadProfileScreen("login")
} else if(deepLinkPath.contains("create-account")){
navigationUtils.loadProfileScreen("create-account")
} else {
navigationUtils.loadProfileScreen()
}
} else if(deepLinkPath.contains("home")) {
navigationUtils.loadHomeScreen()
}
}

fun getIdFromQueryParams(url : String) : String {
val urlQuerySanitizer = UrlQuerySanitizer()
urlQuerySanitizer.allowUnregisteredParamaters = true
urlQuerySanitizer.parseUrl(url)
val id = urlQuerySanitizer.getValue("id")
return id
}
}