Deep linking in WordPress mobile apps: Universal Links, App Links, and custom schemes explained

A push notification that opens the home screen instead of the specific product page. A marketing email link that opens Safari instead of the user’s installed app. A QR code that bounces to the App Store instead of jumping into the right screen. All three are symptoms of broken deep linking — and they leak roughly 70% of the engagement value of every link a mobile app receives. This guide explains how iOS Universal Links, Android App Links, and custom URI schemes actually work in 2026, what breaks them, and how to wire them up correctly for a WordPress-backed app.

Published 2026-05-26 · Reading time: ~12 minutes · iOS 18 + Android 15 · Covers Universal Links, App Links, custom schemes, deferred deep linking

What “deep linking” actually means

A deep link is a URL that, when tapped, jumps directly to a specific in-app screen instead of the app’s home screen. The user does not navigate. They land where the link points — a product page, a chat thread, a saved listing, a checkout step.

Three distinct technologies make this happen, and they are not interchangeable:

TechnologyPlatformURL formWhen the app is installedWhen the app is NOT installed
Universal LinksiOS onlyhttps://brand.com/product/42Opens app directlyOpens Safari to the same URL
App LinksAndroid onlyhttps://brand.com/product/42Opens app directly (after one-time consent)Opens Chrome to the same URL
Custom URI schemeBothbrand://product/42Opens app directlyError / nothing (no fallback)

The strategic implication: if you want a single link to work for both installed-app users AND not-yet-installed users (almost always what you want for marketing), you need Universal Links + App Links, not custom schemes. Custom schemes are now a fallback / legacy mechanism.

Why custom URI schemes lost

Until iOS 9 (2015) and Android 6 (2015), the standard pattern was the custom URI scheme — spotify://album/abc, twitter://user?screen_name=brand. These worked but had three structural problems:

  • No fallback when the app isn’t installed. Tapping brand://product/42 on a device without the brand app installed produced a silent error or “App not found” dialog. You can’t recover the user — they can’t even see the product page on the web.
  • Collision risk. Two apps could register the same scheme. If two banking apps both register bank://, iOS picked one arbitrarily. Users got the wrong app.
  • Phishing risk. Any app could register https://-looking scheme like paypl://. iOS later restricted this, but the trust model was broken.

Apple and Google’s solution was to anchor deep links to real domains via cryptographic verification. The result was Universal Links and App Links — same URL works on web AND in-app, with no collision risk because only the domain owner can register the link.

How iOS Universal Links actually work

The full handshake is more involved than most developers expect:

  1. You host an Apple App Site Association (AASA) file at https://brand.com/.well-known/apple-app-site-association. This file declares which URL paths your app wants to handle.
  2. Apple’s CDN fetches the AASA file when iOS device installs your app. iOS caches it locally.
  3. User taps a link to https://brand.com/product/42 anywhere — Messages, Mail, Safari, a Notes URL, a notification.
  4. iOS checks the cached AASA: does the brand.com domain claim the path /product/*? If yes, route to the app.
  5. The app receives the URL via NSUserActivity with activityType == NSUserActivityTypeBrowsingWeb. Your app’s deep link router parses the URL path and navigates accordingly.

The example AASA file for a WordPress site that wants the app to handle product pages and the user account area:

{
  "applinks": {
    "details": [
      {
        "appIDs": ["TEAMID.com.brand.app"],
        "components": [
          { "/": "/product/*" },
          { "/": "/account/*" },
          { "/": "/articles/*" },
          { "/": "*", "exclude": true,
            "comment": "Do NOT route everything — only matched paths above" }
        ]
      }
    ]
  }
}

Three common failures that ship to production:

  • AASA file served with wrong Content-Type. Must be application/json (or no extension served as JSON). WordPress + most CDNs serve /.well-known/apple-app-site-association as a plain file with no extension, which can default to application/octet-stream — Apple’s CDN rejects this.
  • AASA file behind login or rate limit. Apple’s CDN fetches the file periodically. If it returns 401, 429, or 503, iOS falls back to opening Safari. Users see “deep linking is broken” with no error.
  • Wildcard route on root (/). Routing /* to the app means EVERY brand.com link opens the app — including links to the homepage, the cart, the help center. Users get frustrated. Always declare specific path patterns and exclude the rest.

How Android App Links work (and why they’re harder)

Android App Links solve the same problem but with a more aggressive verification step:

  1. You host assetlinks.json at https://brand.com/.well-known/assetlinks.json. Same well-known location pattern as iOS, different format.
  2. You sign your Android app with a known key. The signing fingerprint must match the one declared in assetlinks.json.
  3. Android verifies the relationship at install time by fetching assetlinks.json over HTTPS and matching the app’s signing fingerprint against the declared fingerprint.
  4. If verification passes, Android grants the app “verified” status for the declared domain. Tapping a link opens the app directly with no chooser dialog.
  5. If verification fails or is incomplete, Android falls back to a chooser (“Open with…”) on every tap, asking the user every time.

Example assetlinks.json:

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.brand.app",
    "sha256_cert_fingerprints": [
      "AA:BB:CC:DD:EE:FF:..."
    ]
  }
}]

The single most common Android App Links failure: wrong SHA-256 fingerprint. When Google Play uses Play App Signing (the default for new apps in 2026), the fingerprint that ships to production is NOT the same as your local debug fingerprint. The fingerprint in assetlinks.json must be the Play App Signing certificate fingerprint — visible in Google Play Console → App integrity → App signing. If you ship with the wrong one, Android falls back to chooser dialog forever.

WordPress-specific challenges

Hosting AASA and assetlinks.json files on a WordPress site has predictable rough edges:

1. WordPress permalink rewrites can swallow /.well-known/

WordPress’s .htaccess default forwards every request that doesn’t match a real file to index.php. If WordPress doesn’t have a route for /.well-known/apple-app-site-association, it returns the 404 template — which is HTML, not JSON. Apple rejects.

The fix is one of three patterns:

  • Server-level rewrite exception: edit .htaccess to serve /.well-known/* as static files, never routed through WordPress.
  • WordPress action handler: hook init, detect the request URI, emit the JSON file content directly with application/json Content-Type, then exit; before WordPress routing.
  • Native CDN endpoint: most modern WordPress hosts (Kinsta, WP Engine, Cloudflare) let you set static-file routes outside WordPress. Use this if available.

The plugin pattern: any time your app builder generates the AASA / assetlinks.json content dynamically (e.g., when the user adds a new theme widget that needs app linking), the file content changes — meaning the static file approach fails. A dynamic handler tied to plugin configuration is more reliable. Plugins like Appress handle this by registering an init hook that intercepts /.well-known/* requests and serves the current configuration as JSON.

2. CDN caching can leave a stale AASA / assetlinks.json file in flight

You update the AASA file when adding a new path pattern. Apple’s CDN fetched the old one 3 hours ago. iOS devices that install your app in the next 24 hours see the old declaration. New paths don’t deep link.

Mitigation:

  • Set Cache-Control: max-age=3600, must-revalidate (or shorter) on the well-known endpoint
  • Purge your CDN edge cache after every AASA / assetlinks update
  • Wait 24 hours after updating before declaring “rolled out”

3. The www. canonical confusion

If your site is served on both brand.com and www.brand.com (with one redirecting to the other), the AASA file MUST be on the canonical domain (the one that does NOT redirect). Apple’s CDN does not follow redirects when fetching the well-known endpoint. If brand.com/.well-known/apple-app-site-association redirects to www.brand.com/..., Apple sees the 301 as failure.

Deep link payloads in push notifications

The single highest-ROI deep linking use case is push notification routing. A WooCommerce order-shipped push that opens to the home screen wastes 80% of its engagement value. A push that opens to the actual order detail page captures it.

The payload pattern is platform-specific but follows the same shape:

// APNs payload (iOS)
{
  "aps": {
    "alert": { "title": "Order shipped", "body": "Your order #4892 is on its way" },
    "sound": "default"
  },
  "url": "https://brand.com/account/orders/4892",
  "order_id": "4892"
}

// FCM payload (Android)
{
  "notification": {
    "title": "Order shipped",
    "body": "Your order #4892 is on its way"
  },
  "data": {
    "url": "https://brand.com/account/orders/4892",
    "order_id": "4892"
  }
}

The app’s push handler reads the url key (or the data.url field on Android), passes it to the deep link router, and the user lands directly on the order page. The WordPress backend that triggered the push (e.g., a woocommerce_order_status_shipped hook) is responsible for populating this URL field. If the WordPress payload-builder forgets to add it, the push still delivers but lands on the home screen — silent quality regression.

This is why Appress hooks the WordPress push trigger to automatically include the post permalink, order URL, or listing URL in every push payload. The deep link router in the app handles routing automatically based on URL pattern matching.

Deferred deep linking — when the user hasn’t installed yet

The hardest deep linking case is the user who clicks a link without the app installed. The flow looks like:

  1. User clicks https://brand.com/product/42 on a marketing email
  2. iOS sees no app installed for the domain → opens Safari
  3. Safari loads the product page (good — the web version is the fallback)
  4. The web page shows a smart App Banner (“Get the app for a better experience”)
  5. User taps “Get app” → App Store opens → user installs
  6. User opens the app for the first time → ❌ the app does NOT know which product the user originally wanted

Step 6 is the failure. Without extra work, the install-after-clicking flow drops the original deep link. The user lands on the app’s home screen instead of product 42, and almost certainly gives up.

Three patterns solve this:

  • Smart App Banners with deep-link parameter (Apple’s native fix). Add app-argument=PATH to the smart banner meta tag. iOS preserves the path across install and passes it to the app on first launch. Works on iOS only.
  • Firebase Dynamic Links / Branch.io (third-party services). Wrap your link in a third-party shortener that handles cross-platform install-and-restore-context flow. Works but adds a vendor dependency, and Firebase Dynamic Links is being deprecated in 2025.
  • Custom postback via clipboard or device fingerprint. When the user clicks the marketing link, the page sets a clipboard token or a server-side device fingerprint. On first launch, the app reads the clipboard / fingerprints itself / asks the backend for any pending deep link associated with this device. Custom but doesn’t rely on a deprecating vendor.

Deferred deep linking is worth implementing only if your acquisition funnel has measurable drop-off between “click marketing link” and “open app for first time”. Most small-to-mid WordPress sites don’t need this complexity. Universal Links + App Links are enough for the installed-user case, which is where the engagement actually compounds.

Testing deep links before shipping

Apple and Google both ship validators. Use them BEFORE submitting to the App Store / Google Play — finding a broken deep link after release is much harder to fix.

iOS Universal Links validator

  • Apple’s own AASA validator: branch.io/resources/aasa-validator (third-party tool that runs Apple’s official format check)
  • On-device: open Notes app, paste your deep link URL, long-press, choose “Open”. If your app opens, the AASA is being honored.
  • Console.app while connecting your device: filter for swcd process — shows AASA fetch errors

Android App Links validator

  • Google’s tool: digital-asset-links/tools/generator (generates AND validates)
  • Command line: adb shell pm verify-app-links --re-verify com.brand.app forces re-verification
  • Check status: adb shell pm get-app-links com.brand.app shows verification result per domain

If validation fails on your verified domain, the problem is usually one of: wrong SHA-256 fingerprint, file not served as application/json, file behind authentication, or HTTPS certificate chain issue. Walk these in order.

How Appress handles deep linking automatically

Deep linking is one of the most error-prone parts of mobile app development — wrong fingerprint, wrong Content-Type, wrong path declaration, all silent failures. Appress handles the entire flow:

  • AASA + assetlinks.json auto-generation — derived from your active integrations (Voxel theme, WooCommerce, Bricks Builder, Avada Builder, Elementor). Paths declared correctly per integration.
  • Files served via WordPress plugin handler — never blocked by WordPress 404 routing or .htaccess rewrites. Content-Type always application/json.
  • Play App Signing fingerprint auto-included — assetlinks.json fingerprint matches what Google Play actually signs your app with, not your local debug certificate.
  • Deep link router built into the native shell — every push notification, every external link, every QR code routes through the same URL-pattern parser, opens the correct in-app screen.
  • WordPress push triggers auto-populate URL field — every woocommerce_order_status_* hook, every post_published, every custom event includes the deep link target by default.

Most builders ship deep linking as an afterthought. Appress treats it as default behavior — because in production, the link IS the engagement.

Frequently asked questions

Do I need both Universal Links AND a custom URI scheme?

In 2026, you only need Universal Links (iOS) and App Links (Android). Custom URI schemes are a fallback for legacy integrations — a third-party app passing you a link via brand:// instead of an HTTPS URL. Modern apps use HTTPS for all deep links and treat schemes as legacy.

What happens if I serve the AASA file as text/plain instead of application/json?

Apple’s CDN rejects the file silently. iOS treats the domain as having no Universal Link configuration and falls back to opening links in Safari. Symptom: deep links work on web but never open the app. Fix: serve as application/json and verify with curl -I.

My Android App Links work in debug builds but break in production. Why?

The most common cause is fingerprint mismatch. Your debug build is signed with your local debug certificate; Google Play production builds are signed with Play App Signing. The SHA-256 fingerprint in assetlinks.json must match the Play App Signing certificate (found in Google Play Console → Setup → App integrity), not your local debug fingerprint.

How long does Apple take to pick up changes to my AASA file?

Apple’s CDN refetches the AASA file roughly every 24 hours, but the actual cache time per device varies. After updating, expect 24 hours for most devices to see the change. If you must propagate faster, ship an app update — installing the new app version forces an immediate AASA refetch on that device.

Can I deep link to a screen that requires the user to be logged in?

Yes — and the app should handle this gracefully. The pattern is: the deep link router resolves the URL → checks if the user is authenticated → if not, redirects to login screen → after login, restores the original deep link target. This requires the deep link router to push the target URL onto a “pending deep link” stack before showing login.

Does deep linking affect SEO?

Indirectly, yes. When users click a deep link from search results and land in your app instead of mobile web, Google sees the strong engagement signal (long session, low bounce rate). Apple Smart App Banners and Google’s app indexing also let search engines understand that the same content exists in your app, which boosts both web and app discoverability.

Do Universal Links work in iOS Mail and iMessage?

Yes — that’s exactly the case they were designed for. Tapping https://brand.com/product/42 in Mail, Messages, Notes, or any other system app opens your app directly if installed. The exception is Safari itself: tapping a link from inside Safari opens the link in Safari, not your app. This is intentional — Apple doesn’t want to interrupt the browser experience.

Should I use Firebase Dynamic Links for deferred deep linking?

As of late 2025, Google has announced Firebase Dynamic Links is being shut down in August 2025. Migrate off. Alternatives: Branch.io (paid, mature), Apple’s Smart App Banner with app-argument (iOS-only but free), or a custom server-side deferred link postback (more work but no vendor lock-in).

Ship a WordPress mobile app with deep linking that just works

Appress generates the AASA file, the assetlinks.json file, and the in-app deep link router automatically — derived from your active WordPress integrations. Push notifications auto-include the correct destination URL. Universal Links pass App Store review on first submission.