A universal link is a special URL type that works on web browsers and mobile apps. It ensures users have a smooth experience no matter what device they use. With universal links, businesses can make it easy for their users to directly open Android and iOS apps from delivered emails and access their content on any device via only one link that works everywhere.
Universal links work as follows:
- Opens app or web: If a user has the app, the link opens the app. If not, it opens the web.
- Offers better user experience: Users get to the right content quickly and easily.
This article explains how the universal link setup process works, provides SSL setup examples for multiple providers such as AWS, Google Cloud, etc., and the following steps to complete the setup process:
1. Configure link branding for your tracking link
2. Handling asset files
3. Verification
4. Application
5. Link resolution
2. Handling asset files
For universal links to work, you need to include your application-specific asset files under the subdomain you set up on the DNS record setup step. Your setup must include route prioritization to re-route the incoming requests to assetlinks.json (Android) and apple-app-site-association (iOS) to your file server (or cloud storage) and everything else to sendgrid.net.
Adding the asset files under the subdomain will ensure that your app is opened instead of the web page every time it is clicked. For example, if your links are branded as https://links.acme.com, your asset files should be available as https://links.acme.com/.well-known/assetlinks.json, https://links.acme.com/.well-known/apple-app-site-association, and https://links.acme.com/apple-app-site-association (without the JSON extension).
The sample case above would look as follows:
| Precedence | Path | Origin | Protocol |
|---|---|---|---|
| 0 | apple-site-association | File Server | HTTPS Only |
| 1 | .well-known/apple-site-association | File Server | HTTPS Only |
| 2 | .well-known/assetlinks.json | File Server | HTTPS Only |
| 3 | * | sendgrid.net | HTTP and HTTPS |
Below you can see some examples of different providers.
NGINX
server {
listen 80;
listen 443 ssl;
server_name 'links.acme.com';
ssl_certificate '/etc/pki/tls/certs/links.acme.com.crt';
ssl_certificate_key '/etc/pki/tls/private/links.acme.com.key';
location = /apple-app-site-association {
root '/var/www/links.acme.com';
default_type 'application/json';
}
location = /.well-known/apple-app-site-association {
root '/var/www/links.acme.com';
default_type 'application/json';
}
location = /.well-known/assetlinks.json {
root '/var/www/links.acme.com';
default_type 'application/json';
}
location / {
proxy_pass 'https://sendgrid.net';
proxy_set_header 'Host' 'links.example.com';
}
}AWS Route53
Refer to the Universal Link Setup page to set up universal links using Route53, S3, and Cloudfront.
GCP
For Google Cloud, you can utilize routes or use URL maps on your load balancer to handle the traffic.
3. Verification
If you set up your asset configuration internally, you can use a bastion server. If it's a public setup, you can directly test your asset links and routing. In either case, testing will be the same.
[must return JSON response]
curl http://internal.domain/.well-known/assetlinks.json
curl http://internal.domain/.well-known/apple-app-site-association
curl http://internal.domain/apple-app-site-association
[must return 404 response from sendgrid.net]
curl http://internal.domain/test-404-response4. Application
Once you’ve verified that the asset links are set up correctly, you can replace sendgrid.net in your DNS setup with your internal setup.
links.acme.com => sendgrid.com
// will be updated as
links.acme.com => example.internal.load-balancer.com
// or
links.acme.com => cloudfront.com/12345
// or
links.acme.com => 127.0.0.1 (NGINX instance IP)
5. Link resolution
Now you can trigger your application directly from the email links. However, the links are encrypted and the destination is unavailable. Link resolution is required as the last step of the implementation for your application to redirect users to the relevant pages correctly within your application.
To complete this action, follow these three steps:
- Send an HTTP request to the link the user clicks to resolve the destination link.
- Obtain the destination from the sent request.
- Redirect the user to the relevant page in your application.
This will ensure that the user will be redirected to the destination page and the click log is delivered to the Insider server.
Below you can see some examples:
Android link resolution example
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
onNewIntent(getIntent());
}
protected void onNewIntent(Intent intent) {
String action = intent.getAction();
final String encodedURL = intent.getDataString();
if (Intent.ACTION_VIEW.equals(action) && encodedURL != null) {
Log.d("App Link", encodedURL);
new Thread(new Runnable() {
public void run() {
try {
URL originalURL = new URL(encodedURL);
HttpURLConnection ucon = (HttpURLConnection) originalURL.openConnection();
ucon.setInstanceFollowRedirects(false);
URL resolvedURL = new URL(ucon.getHeaderField("Location"));
Log.d("App Link", resolvedURL.toString());
}
catch (MalformedURLException ex) {
Log.e("App Link",Log.getStackTraceString(ex));
}
catch (IOException ex) {
Log.e("App Link",Log.getStackTraceString(ex));
}
}
}).start();
}
}
iOS link resolution examples
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
guard let encodedURL = userActivity.webpageURL else {
print("Unable to handle user activity: No URL provided")
return false
}
let task = URLSession.shared.dataTask(with: encodedURL, completionHandler: { (data, response, error) in
guard let resolvedURL = response?.url else {
print("Unable to handle URL: \(encodedURL.absoluteString)")
return
}
// Now you have the resolved URL that you can
// use to navigate somewhere in the app.
print(resolvedURL)
})
task.resume()
}
return true
}- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler {
if (userActivity.activityType == NSUserActivityTypeBrowsingWeb) {
NSURL *encodedURL = userActivity.webpageURL;
if (encodedURL == nil) {
NSLog(@"Unable to handle user activity: No URL provided");
return false;
}
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithURL:encodedURL completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (response == nil || [response URL] == nil) {
NSLog(@"Unable to handle URL: %@", encodedURL.absoluteString);
return;
}
// Now you have the resolved URL that you can
// use to navigate somewhere in the app.
NSURL *resolvedURL = [response URL];
NSLog(@"Original URL: %@", resolvedURL.absoluteString);
}];
[task resume];
}
return YES;
}If you’re using an external company to handle your universal links such as branch.io, you might want to consult with the respective vendors on how to set up the universal links. If you have a mixed setup where you use both your own links and an external company setup, you need to complete all the steps listed in this guide and contact your provider to set it up with them as well.