{"id":1893,"date":"2026-01-15T23:34:00","date_gmt":"2026-01-15T23:34:00","guid":{"rendered":"https:\/\/dev95.site\/universal-links-at-scale-the-challenges-nobody-talks-about\/"},"modified":"2026-01-15T23:34:00","modified_gmt":"2026-01-15T23:34:00","slug":"universal-links-at-scale-the-challenges-nobody-talks-about","status":"publish","type":"post","link":"https:\/\/dev95.site\/ar\/universal-links-at-scale-the-challenges-nobody-talks-about\/","title":{"rendered":"Universal Links At Scale: The Challenges Nobody Talks About"},"content":{"rendered":"<div id=\"dev95-3275898165\" class=\"dev95-- dev95-entity-placement\"><script async=\"async\" data-cfasync=\"false\" src=\"https:\/\/pl27862732.profitableratecpmnetwork.com\/2ad7a50e0bbc23ac6801d7b77c501463\/invoke.js\"><\/script>\r\n<div id=\"container-2ad7a50e0bbc23ac6801d7b77c501463\"><\/div><\/div><div>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-text\"><i><em class=\"italic\" style=\"white-space: pre-wrap;\">Originally published on the\u00a0<\/em><\/i><a href=\"https:\/\/medium.com\/justeattakeaway-tech\/universal-links-at-scale-the-challenges-nobody-talks-about-bab45d557d8b?ref=albertodebortoli.com\" rel=\"noreferrer\"><i><em class=\"italic\" style=\"white-space: pre-wrap;\">Just Eat\u00a0Takeaway\u00a0Engineering Blog<\/em><\/i><\/a><i><em class=\"italic\" style=\"white-space: pre-wrap;\">.<\/em><\/i><\/div>\n<\/div>\n<p><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2026\/01\/0_5RZ69ENMLrgyZwGV.webp?w=1280&#038;ssl=1\" alt=\"Universal Links At Scale: The Challenges Nobody Talks About\"><\/p>\n<p>Universal Links have been around since iOS 9 (2015), yet the topic remains surprisingly underrated in the iOS community. While most developers understand the basic concept (associating your website with your app so links open directly in the app) the practical challenges of implementing and maintaining Universal Links at scale are rarely discussed.<\/p>\n<p>When a user taps a Universal Link, iOS checks if the domain is associated with any installed app. If it is, iOS opens the app directly. If not, it opens the link in the browser. This &#8220;universal&#8221; behavior makes them superior to custom URL schemes (deep links) for user-facing communications.<\/p><div id=\"dev95-1886730787\" class=\"dev95- dev95-entity-placement\"><center>\r\n<script>\r\n  atOptions = {\r\n    'key' : '4ba6b6513c00e0ba76511f798ae56401',\r\n    'format' : 'iframe',\r\n    'height' : 50,\r\n    'width' : 320,\r\n    'params' : {}\r\n  };\r\n<\/script>\r\n<script src=\"https:\/\/www.highrevenueformat.com\/4ba6b6513c00e0ba76511f798ae56401\/invoke.js\"><\/script>\r\n\t<\/center><\/div>\n<p>However, despite their importance, many developers treat AASA files as simple configuration files, overlooking the complex challenges involved in validating, testing, and maintaining them at scale.<\/p>\n<p>In 2024, I put a lot of effort into crafting a solid solution for some overlooked challenges surrounding universal links. Every time I refer back to that work, I am impressed by how well it has served the company, which constantly renews my desire to write about it. GenAI has become incredibly helpful with the drafting process, so I finally have no excuse not to share this story!<\/p>\n<p>In this post, I&#8217;ll walk through the real-world challenges I&#8217;ve encountered and the solutions I&#8217;ve developed over the years working with Universal Links across multiple web domains and localized applications.<\/p>\n<h2 id=\"the-basics-what-makes-universal-links-work\">The Basics: What Makes Universal Links Work?<\/h2>\n<p>Universal Links work through a combination of three pieces:<\/p>\n<ol>\n<li><strong>Associated Domains Entitlement<\/strong> in your app (the <code>.entitlements<\/code> file)<\/li>\n<li><strong>Apple App Site Association (AASA) file<\/strong> on your website<\/li>\n<li><strong>Proper handling<\/strong> of incoming links in your app code<\/li>\n<\/ol>\n<p>The AASA file must be served from <code>\/.well-known\/apple-app-site-association<\/code> over HTTPS, without redirects, and with the correct content type (<code>application\/json<\/code>).<\/p>\n<p>Here&#8217;s a simple AASA file:<\/p>\n<pre><code class=\"language-json\">{\n  \"applinks\": {\n    \"details\": [\n      {\n        \"appIDs\": [\"TEAMID.com.example.app\"],\n        \"components\": [\n          { \"\/\": \"\/account\/login\" },\n          { \"\/\": \"\/restaurants\/*\" }\n        ]\n      }\n    ]\n  }\n}<\/code><\/pre>\n<p>What most tutorials don&#8217;t tell you is that this is just the beginning. Real-world AASA files are far more complex, and validating them is a challenge in itself. The reality gets complicated when you need to:<\/p>\n<ul>\n<li>Validate that your AASA file respects a schema<\/li>\n<li>Test links before deploying to production<\/li>\n<li>Handle dynamic URL patterns with substitution variables<\/li>\n<li>Ensure Apple&#8217;s CDN has picked up your latest changes<\/li>\n<li>Parse and match wildcard patterns correctly<\/li>\n<li>Handle encoding and special characters<\/li>\n<\/ul>\n<h2 id=\"challenge-1-nobody-validates-against-a-json-schema\">Challenge 1: Nobody Validates Against a JSON Schema<\/h2>\n<p>Here&#8217;s a dirty secret:\u00a0<strong>most AASA files in production have never been validated against a schema<\/strong>. Teams deploy files, hope for the best, and only discover issues when links stop working.<\/p>\n<p><strong>Why It Matters:<\/strong>\u00a0An invalid AASA file might be served successfully but fail to associate your app with your website. iOS won&#8217;t throw errors; Universal Links simply won&#8217;t work, and you might not notice until users report issues.<\/p>\n<p>Online validators like\u00a0<a href=\"https:\/\/branch.io\/resources\/aasa-validator\/?ref=albertodebortoli.com\">branch.io\/resources\/aasa-validator<\/a>\u00a0and\u00a0<a href=\"https:\/\/getuniversal.link\/?ref=albertodebortoli.com\">getuniversal.link<\/a>\u00a0check basic accessibility and JSON parsing, but they don&#8217;t validate the actual schema. A file can be valid JSON yet completely invalid as an AASA file.<\/p>\n<h3 id=\"the-solution-json-schema-validation-in-ci\">The Solution: JSON Schema Validation in CI<\/h3>\n<p>Create a comprehensive JSON Schema that validates the entire AASA structure, including:<\/p>\n<ul>\n<li>Required fields (<code>applinks<\/code>,\u00a0<code>details<\/code>,\u00a0<code>appIDs<\/code>,\u00a0<code>components<\/code>)<\/li>\n<li>Optional fields (<code>substitutionVariables<\/code>,\u00a0<code>exclude<\/code>,\u00a0<code>caseSensitive<\/code>,\u00a0<code>percentEncoded<\/code>)<\/li>\n<li>Proper nesting and data types<\/li>\n<li>Support for other AASA features (<code>webcredentials<\/code>,\u00a0<code>appclips<\/code>,\u00a0<code>activitycontinuation<\/code>)<\/li>\n<\/ul>\n<p>Here&#8217;s a schema that defines the correct structure of an AASA file (just for the <code>applinks<\/code> section) :<\/p>\n<pre><code class=\"language-json\">{\n  \"$schema\": \"https:\/\/json-schema.org\/draft\/2020-12\/schema\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"applinks\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"defaults\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"caseSensitive\": {\n              \"type\": \"boolean\"\n            },\n            \"percentEncoded\": {\n              \"type\": \"boolean\"\n            }\n          }\n        },\n        \"details\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"appIDs\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n              \"components\": {\n                \"type\": \"array\",\n                \"items\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"\/\": { \"type\": \"string\" },\n                    \"?\": { \"type\": \"object\" },\n                    \"#\": { \"type\": \"string\" },\n                    \"exclude\": { \"type\": \"boolean\" },\n                    \"caseSensitive\": { \"type\": \"boolean\" },\n                    \"percentEncoded\": { \"type\": \"boolean\" }\n                  },\n                  \"required\": [\"\/\"]\n                }\n              },\n              \"defaults\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"caseSensitive\": { \"type\": \"boolean\" },\n                  \"percentEncoded\": { \"type\": \"boolean\" }\n                }\n              }\n            }\n          }\n        },\n        \"substitutionVariables\": { \"type\": \"object\" }\n      },\n      \"required\": [\"details\"]\n    }\n  },\n  \"required\": [\"applinks\"]\n}\n<\/code><\/pre>\n<p>Integrating schema validation into your CI pipeline ensures invalid files never reach production. This catches issues like:<\/p>\n<ul>\n<li>Missing required fields<\/li>\n<li>Wrong types (string instead of array)<\/li>\n<li>Typos in property names (which would be silently ignored)<\/li>\n<li>Invalid component structures<\/li>\n<\/ul>\n<p>You might want to consider building a Swift CLI tool with Argument Parser, in which case I would suggest using <a href=\"https:\/\/github.com\/kylef\/JSONSchema.swift?ref=albertodebortoli.com\">JSONSchema.swift<\/a>.<\/p>\n<h2 id=\"challenge-2-the-apple-cdn-layer\">Challenge 2: The Apple CDN Layer<\/h2>\n<p>Here&#8217;s something that surprises many developers:\u00a0<strong>iOS doesn&#8217;t fetch the AASA file directly from your website<\/strong>. Instead, Apple operates a CDN that caches AASA files from websites.<\/p>\n<p>The CDN URL follows this pattern:<\/p>\n<pre><code>https:\/\/app-site-association.cdn-apple.com\/a\/v1\/&lt;domain&gt;\n<\/code><\/pre>\n<p>For example, for\u00a0<code>just-eat.co.uk<\/code>:<\/p>\n<ul>\n<li><strong>Website<\/strong>:\u00a0<code>https:\/\/just-eat.co.uk\/.well-known\/apple-app-site-association<\/code><\/li>\n<li><strong>Apple CDN<\/strong>:\u00a0<code>https:\/\/app-site-association.cdn-apple.com\/a\/v1\/just-eat.co.uk<\/code><\/li>\n<\/ul>\n<p><strong>Why It Matters:<\/strong>\u00a0This caching happens periodically (every few hours), and there&#8217;s no guarantee that your latest changes are immediately available. If your website&#8217;s AASA file differs from what&#8217;s cached on Apple&#8217;s CDN, Universal Links may not work as expected. You might deploy a fix, but iOS devices could still be using the old cached version for hours or even days.<\/p>\n<h3 id=\"the-solution-cdn-validation\">The Solution: CDN Validation<\/h3>\n<p>To ensure your AASA file has propagated correctly, you need to compare the file on your website with the one on Apple&#8217;s CDN. This validates that:<\/p>\n<ol>\n<li>Your file is publicly accessible and has a valid SSL certificate<\/li>\n<li>The file has the correct MIME type (<code>application\/json<\/code>)<\/li>\n<li>Apple&#8217;s CDN has successfully cached your latest version<\/li>\n<\/ol>\n<p>Here&#8217;s a validator that does exactly this:<\/p>\n<pre><code class=\"language-swift\">struct AASAContent: Equatable, Decodable {\n    let appLinks: AppLinks\n    \n    enum CodingKeys: String, CodingKey {\n        case appLinks = \"applinks\"\n    }\n\n    \/\/ and nested Decodable structs\n}\n\nenum AASAFileLocation {\n    case website\n    case appleCdn\n\n    func buildURL(with domain: Domain) throws -&gt; URL {\n        switch self {\n        case .website:\n            return URL(string: \"https:\/\/(domain)\")!\n                .appendingPathComponent(\".well-known\")\n                .appendingPathComponent(\"apple-app-site-association\")\n        case .appleCdn:\n            return URL(string: \"https:\/\/app-site-association.cdn-apple.com\/a\/v1\/\")!\n                .appendingPathComponent(domain)\n        }\n    }\n}\n    \nfunc validateCDN(for domain: Domain) async throws {\n    let websiteURL = try AASAFileLocation.website.buildURL(with: domain)\n    let appleCdnURL = try AASAFileLocation.appleCdn.buildURL(with: domain)\n\n    let domainFile: AASAContent = try await downloadFile(url: websiteURL)\n    let appleFile: AASAContent = try await downloadFile(url: appleCdnURL)\n\n    guard domainFile == appleFile else {\n        throw ValidateCDNError.fileMismatch(domain: domain)\n    }\n}\n<\/code><\/pre>\n<p>Running this validation daily in CI ensures you&#8217;re alerted when CDN synchronization fails or is delayed. A daily automated check can alert you if there&#8217;s a mismatch, allowing you to investigate and resolve issues before they impact users. This simple check has prevented numerous incidents where teams assumed links were working when they weren&#8217;t.<\/p>\n<h3 id=\"developer-mode-bypass\">Developer Mode Bypass<\/h3>\n<p>For development and debugging, iOS offers a bypass. By adding\u00a0<code>?mode=developer<\/code>\u00a0to your associated domain:<\/p>\n<pre><code class=\"language-xml\">&lt;string&gt;applinks:just-eat.co.uk?mode=developer&lt;\/string&gt;\n<\/code><\/pre>\n<p>Debug builds should use a specific entitlements file where the developer mode is used. Debug builds will fetch the AASA file directly from your domain, bypassing the CDN. This requires enabling &#8220;Associated Domains Development&#8221; in iOS Settings \u2192 Developer. App Store builds always use the CDN and their entitlements file shouldn&#8217;t mention the developer mode.<\/p>\n<h2 id=\"challenge-3-regular-expression-parsing-and-pattern-matching\">Challenge 3: Regular Expression Parsing and Pattern Matching<\/h2>\n<p>The AASA file supports powerful pattern matching through wildcards for flexible URL matching. However, these patterns aren&#8217;t standard regex and use Apple&#8217;s own pattern syntax that needs to be converted to regular expressions for validation.<\/p>\n<p><strong>The Pattern Syntax:<\/strong><\/p>\n<ul>\n<li><code>*<\/code>\u00a0matches zero or more characters (converted to\u00a0<code>.*<\/code>\u00a0in regex)<\/li>\n<li><code>?<\/code>\u00a0matches exactly one character (converted to\u00a0<code>.<\/code>\u00a0in regex)<\/li>\n<li><code>?*<\/code>\u00a0matches one or more characters (converted to\u00a0<code>.+<\/code>\u00a0in regex)<\/li>\n<li><code>*?<\/code>\u00a0also matches one or more characters (converted to\u00a0<code>.+<\/code>\u00a0in regex)<\/li>\n<\/ul>\n<p><strong>The Problem:<\/strong>\u00a0I couldn&#8217;t find any online tool or open-source library implementing Apple&#8217;s matching logic. If you want to validate that specific URLs match your AASA file patterns (for testing or regression prevention), you need to correctly parse and convert these patterns. Online validators like <a href=\"https:\/\/branch.io\/resources\/aasa-validator\/?ref=albertodebortoli.com\">Branch.io&#8217;s AASA validator<\/a> don&#8217;t support this matching logic, they only validate the file structure.<\/p>\n<h3 id=\"the-solution-implementing-apples-matching-logic\">The Solution: Implementing Apple&#8217;s Matching Logic<\/h3>\n<p>I built a custom validator that implements the matching rules. The key insight is that Apple&#8217;s wildcards map to regular expressions. A na\u00efve conversion (where the order of substitutions is important) would look like this:<\/p>\n<pre><code class=\"language-swift\">extension String {\n    var regEx: String {\n        self\n            \/\/ One or more characters\n            .replacingOccurrences(of: \"?*\", with: \".+\")\n            \n            \/\/ One or more characters\n            .replacingOccurrences(of: \"*?\", with: \".+\")\n            \n            \/\/ Zero or more characters\n            .replacingOccurrences(of: \"*\", with: \".*\")\n            \n            \/\/ Exactly one character\n            .replacingOccurrences(of: \"?\", with: \".\")\n    }\n}<\/code><\/pre>\n<p>Additionally, you need to handle URL components properly. For example, if a pattern specifies only a path (<code>\/restaurants\/*<\/code>), you should still match URLs that have query parameters or fragments, unless explicitly excluded. This requires careful construction of the regex pattern to account for optional components, which to be completely honest was very tricky to implement by hand at a time when LLMs weren&#8217;t too helpful.<\/p>\n<h2 id=\"challenge-4-the-substitutionvariables-problem\">Challenge 4: The\u00a0<code>substitutionVariables<\/code>\u00a0Problem<\/h2>\n<p>Apple supports\u00a0<a href=\"https:\/\/developer.apple.com\/documentation\/bundleresources\/applinks\/substitutionvariables-swift.dictionary?ref=albertodebortoli.com\"><code>applinks.substitutionVariables<\/code><\/a>\u00a0for dynamic URL matching. This feature allows you to define variables that can be used in path, query, and fragment components. Substitution variables are particularly helpful to reduce duplication when dealing with URLs that are localised per language. However,\u00a0I couldn&#8217;t find any online validator or open-source tool to support validating links against AASA files that use substitution variables.<\/p>\n<p>Here&#8217;s a real-world example from a multi-language website:<\/p>\n<pre><code class=\"language-json\">{\n  \"applinks\": {\n    \"substitutionVariables\": {\n      \"menu\": [\"speisekarte\", \"menu\"],\n      \"stamp-cards\": [\"stempelkarten\", \"stamp-cards\", \"cartes-\u00e9pargne\", \"stempelkaarten\"]\n    },\n    \"details\": [{\n      \"appIDs\": [\"TEAMID.com.example.app\"],\n      \"components\": [\n        { \"\/\": \"\/$(lang)\/$(menu)\/?*\" },\n        { \"\/\": \"\/\", \"#\": \"$(stamp-cards)\" },\n        { \"\/\": \"\/\", \"#\": \"$(order-history)\" }\n      ]\n    }]\n  }\n}\n<\/code><\/pre>\n<p>The pattern\u00a0<code>\/$(lang)\/$(menu)\/?*<\/code>\u00a0should match URLs like:<\/p>\n<ul>\n<li><code>\/de\/speisekarte\/restaurant-name<\/code><\/li>\n<li><code>\/en\/menu\/restaurant-name<\/code><\/li>\n<li><code>\/fr\/menu\/pizza-place<\/code><\/li>\n<\/ul>\n<p>And\u00a0<code>\/#$(stamp-cards)<\/code>\u00a0should match:<\/p>\n<ul>\n<li><code>\/#stempelkarten<\/code><\/li>\n<li><code>\/#stamp-cards<\/code><\/li>\n<li><code>\/#cartes-\u00e9pargne<\/code><\/li>\n<\/ul>\n<p><strong>Why It Matters:<\/strong>\u00a0Without proper support for substitution variables, you can&#8217;t validate that your Universal Links work correctly. You might think a URL should match, but if the substitution isn&#8217;t handled correctly, it won&#8217;t.<\/p>\n<h3 id=\"the-solution-substitution-variable-expansion\">The Solution: Substitution Variable Expansion<\/h3>\n<p>Implement substitution variable expansion before pattern matching. Here is a trimmed down example:<\/p>\n<ol>\n<li><strong>Parse substitution variables<\/strong>\u00a0from the AASA file<\/li>\n<li><strong>Replace variable references<\/strong>\u00a0(<code>$(variableName)<\/code>) with regex alternatives of their possible values<\/li>\n<li><strong>Handle default variables<\/strong>\u00a0like\u00a0<code>$(lang)<\/code>\u00a0and\u00a0<code>$(region)<\/code>\u00a0which match any two characters<\/li>\n<li><strong>Apply the expanded pattern<\/strong>\u00a0to URL matching after converting Apple&#8217;s pattern syntax to standard regex<\/li>\n<\/ol>\n<p>The key insight is that substitution variables create a disjunction (OR) of possible values:<\/p>\n<pre><code class=\"language-swift\">func replaceWithSubstitutionVariables(_ substitutionVariables: [String: [String]]) -&gt; String {\n    var modifiedString = self\n    let substitutionVariablesWithDefaults = substitutionVariables.merging(defaultSubstitutionVariables) { (current, _) in current }\n    \n    for (key, values) in substitutionVariablesWithDefaults {\n        let pattern = \"\\$\\((key)\\)\"\n        let replacement = \"((values.joined(separator: \"|\")))\"\n        \/\/ Replace $(key) with (value1|value2|value3)\n        modifiedString = modifiedString.replacingOccurrences(\n            of: pattern, \n            with: replacement, \n            options: .regularExpression\n        )\n    }\n    return modifiedString\n}\n\nprivate var defaultSubstitutionVariables: [String: [String]] {\n    [\n        \"lang\": [\"..\"],\n        \"region\": [\"..\"]\n    ]\n}\n<\/code><\/pre>\n<p>So\u00a0<code>\/$(lang)\/$(menu)\/?*<\/code>\u00a0with the variables above becomes:<\/p>\n<pre><code>\/(..)\/((speisekarte|menu))\/.+\n<\/code><\/pre>\n<p>Note:\u00a0<code>$(lang)<\/code>\u00a0and\u00a0<code>$(region)<\/code>\u00a0are special default variables Apple provides that match any two characters.<\/p>\n<p><strong>The order of operations matters<\/strong>: first expand substitution variables, then convert Apple&#8217;s pattern syntax (<code>*<\/code>,\u00a0<code>?<\/code>,\u00a0<code>?*<\/code>) to standard regex. This ensures that wildcards within substitution variable values are handled correctly.<\/p>\n<h3 id=\"the-full-matching-pipeline\">The Full Matching Pipeline<\/h3>\n<p>The complete validation process:<\/p>\n<ol>\n<li>Parse the AASA file and extract components for the target bundle ID<\/li>\n<li>For each component, build a regex pattern by:\n<ul>\n<li>Replacing substitution variables with alternations<\/li>\n<li>Converting <code>*<\/code> and <code>?<\/code> to regex equivalents<\/li>\n<li>Handling paths, query parameters, and fragments<\/li>\n<\/ul>\n<\/li>\n<li>Match incoming URLs against these patterns<\/li>\n<li>Account for the\u00a0<code>exclude<\/code>\u00a0flag that explicitly prevents matching<\/li>\n<\/ol>\n<p>The matcher also needs to handle edge cases:<\/p>\n<ul>\n<li>URLs with query parameters not specified in the component (allowed)<\/li>\n<li>URLs with fragments not specified in the component (allowed)<\/li>\n<li>The\u00a0<code>exclude: true<\/code>\u00a0flag that creates negative matches<\/li>\n<li>Case sensitivity settings<\/li>\n<li>Percent encoding<\/li>\n<\/ul>\n<p>Here&#8217;s the core matching logic:<\/p>\n<pre><code class=\"language-swift\">enum AllowPolicy {\n    case allowed\n    case notAllowed\n}\n\nfunc validateDeepLinking(\n    policy: AllowPolicy,\n    for url: URL,\n    domain: Domain,\n    components: [AASAContent.AppLinks.Detail.Component],\n    substitutionVariables: AASAContent.AppLinks.SubstitutionVariables\n) throws {\n    switch policy {\n    case .allowed:\n        for component in components {\n            let regEx = try regEx(for: component, substitutionVariables: substitutionVariables, on: domain)\n            if findMatch(for: url, in: regEx) {\n                if component.exclude != true {\n                    return\n                } else {\n                    throw ValidateUniversalLinkError.excludedUniversalLink(url: url)\n                }\n            }\n        }\n        throw ValidateUniversalLinkError.unhandledUniversalLink(url: url)\n    case .notAllowed:\n        for component in components {\n            let regEx = try regEx(for: component, substitutionVariables: substitutionVariables, on: domain)\n            if findMatch(for: url, in: regEx) {\n                if component.exclude == true {\n                    return\n                } else {\n                    throw ValidateUniversalLinkError.incorrectlyHandledUniversalLink(url: url)\n                }\n            }\n        }\n    }\n}\n<\/code><\/pre>\n<p><strong>Important<\/strong>: Components are evaluated in order, and the first match wins. This means exclusion rules must come before the broader patterns they&#8217;re excluding from.<\/p>\n<h2 id=\"challenge-5-testing-before-production\">Challenge 5: Testing Before Production<\/h2>\n<p>One of the trickiest aspects of Universal Links is testing. You can&#8217;t just deploy to production and hope it works. Testing requires the AASA file to be hosted on a real domain with proper SSL certificates. You can&#8217;t just test locally or in a simulator without additional setup.<\/p>\n<p><strong>Why It Matters:<\/strong>\u00a0Deploying untested AASA changes to production can break Universal Links for all users. Since Apple caches AASA files, fixing issues can take hours or days to propagate.<\/p>\n<h3 id=\"the-solution-a-staging-environment-with-real-domains\">The Solution: A Staging Environment with Real Domains<\/h3>\n<p>Set up a staging environment using AWS infrastructure (or similar):<\/p>\n<ol>\n<li><strong>S3 bucket<\/strong>\u00a0to host AASA files<\/li>\n<li><strong>CloudFront distributions<\/strong>\u00a0for each staging domain with HTTPS<\/li>\n<li><strong>Route53 records<\/strong>\u00a0pointing staging subdomains to CloudFront<\/li>\n<\/ol>\n<p>The staging domains follow a pattern like:<\/p>\n<pre><code>lieferando-de.aasa-staging.mobile-team.example.com\n<\/code><\/pre>\n<p>This mirrors the production domain\u00a0<code>lieferando.de<\/code>\u00a0and serves the same AASA file structure.<\/p>\n<p>Debug builds include both staging and production domains in its entitlements:<\/p>\n<pre><code class=\"language-xml\">&lt;key&gt;com.apple.developer.associated-domains&lt;\/key&gt;\n&lt;array&gt;\n    &lt;string&gt;applinks:lieferando-de.aasa-staging.mobile-team.example.com&lt;\/string&gt;\n    &lt;string&gt;applinks:lieferando.de&lt;\/string&gt;\n    &lt;string&gt;applinks:www.lieferando.de&lt;\/string&gt;\n&lt;\/array&gt;\n<\/code><\/pre>\n<p>Now you can test a link like:<\/p>\n<pre><code>https:\/\/lieferando-de.aasa-staging.mobile-team.example.com\/menu\/pizzeria\n<\/code><\/pre>\n<p>Instead of:<\/p>\n<pre><code>https:\/\/lieferando.de\/menu\/pizzeria\n<\/code><\/pre>\n<p>The staging URL opens your debug\/ad-hoc build exactly like the production URL would open your App Store build, but without risking production changes.<\/p>\n<h3 id=\"important-testing-considerations\">Important Testing Considerations<\/h3>\n<ul>\n<li><strong>TestFlight builds<\/strong>\u00a0are production builds and therefore use production entitlements and won&#8217;t work with staging domains<\/li>\n<li><strong>Ad-hoc and debug builds<\/strong>\u00a0can include staging domains<\/li>\n<li><strong>Simulator testing<\/strong>\u00a0has limitations. Universal Links work best on physical devices<\/li>\n<li><strong>Developer mode<\/strong>\u00a0must be enabled on device for direct AASA fetching (bypassing CDN)<\/li>\n<li>For\u00a0<strong>non-debug builds<\/strong>, you&#8217;ll need to wait for Apple&#8217;s CDN to cache your staging AASA file<\/li>\n<\/ul>\n<h2 id=\"challenge-6-regression-testing-and-comprehensive-link-validation\">Challenge 6: Regression Testing and Comprehensive Link Validation<\/h2>\n<p>Manual testing doesn&#8217;t scale. Every change to the AASA file needs verification across dozens or hundreds of URLs. And you need to verify both:<\/p>\n<ol>\n<li>URLs that\u00a0<strong>should<\/strong>\u00a0open the app (deep-linkable)<\/li>\n<li>URLs that\u00a0<strong>should not<\/strong>\u00a0open the app (non-deep-linkable, excluded)<\/li>\n<li>Complex URLs with query parameters, fragments, and wildcards<\/li>\n<\/ol>\n<p><strong>Why It Matters:<\/strong>\u00a0Without comprehensive validation, you might:<\/p>\n<ul>\n<li>Miss URLs that should deep link but don&#8217;t<\/li>\n<li>Accidentally deep link URLs that should open in the browser<\/li>\n<li>Break existing functionality when making changes<\/li>\n<\/ul>\n<h3 id=\"the-solution-automated-content-validation\">The Solution: Automated Content Validation<\/h3>\n<p>I maintain JSON files alongside each AASA file listing the expected behavior:<\/p>\n<pre><code class=\"language-json\">{\n  \"deep_linkable_urls\": [\n    \"https:\/\/lieferando.de\/\",\n    \"https:\/\/lieferando.de\/punkte\",\n    \"https:\/\/lieferando.de\/#stempelkarten\",\n    \"https:\/\/lieferando.de\/en#stamp-cards\",\n    \"https:\/\/lieferando.de\/lieferservice\/essen\/berlin-10115\",\n    \"https:\/\/lieferando.de\/en\/delivery\/food\/berlin-10115\"\n  ],\n  \"non_deep_linkable_urls\": [\n    \"https:\/\/lieferando.de\/?openOnWeb=true\",\n    \"https:\/\/lieferando.de\/anyPath?openOnWeb=true\"\n  ]\n}\n<\/code><\/pre>\n<p>The validator ensures:<\/p>\n<ul>\n<li>Every URL in\u00a0<code>deep_linkable_urls<\/code>\u00a0matches a non-excluded component<\/li>\n<li>Every URL in\u00a0<code>non_deep_linkable_urls<\/code>\u00a0either doesn&#8217;t match or matches an excluded component<\/li>\n<\/ul>\n<p>This runs on CI on every pull request. Changes to the AASA file must include updates to the expected URLs, creating living documentation of what&#8217;s supported.<\/p>\n<h3 id=\"error-types\">Error Types<\/h3>\n<p>The validator catches several error conditions:<\/p>\n<ol>\n<li><strong>Unhandled URL<\/strong>: A URL expected to be deep-linkable doesn&#8217;t match any component<\/li>\n<li><strong>Excluded URL<\/strong>: A URL expected to be deep-linkable matches an excluded component<\/li>\n<li><strong>Incorrectly Handled URL<\/strong>: A URL expected to be non-deep-linkable actually matches a component<\/li>\n<\/ol>\n<p>Each error provides clear diagnostics:<\/p>\n<pre><code class=\"language-swift\">enum ValidateUniversalLinkError: Error {\n    case unhandledUniversalLink(url: URL)\n    case excludedUniversalLink(url: URL)\n    case incorrectlyHandledUniversalLink(url: URL)\n    \/\/ ...\n}\n<\/code><\/pre>\n<h2 id=\"challenge-7-encoding-and-special-characters\">Challenge 7: Encoding and Special Characters<\/h2>\n<p>Universal Links often contain special characters, especially for localized content:<\/p>\n<pre><code>https:\/\/lieferando.at\/fr#cartes-\u00e9pargne\n<\/code><\/pre>\n<p>The fragment\u00a0<code>cartes-\u00e9pargne<\/code>\u00a0contains an accented character. Here&#8217;s the critical rule:<\/p>\n<p><strong>Universal Links must NOT be percent-encoded in the AASA file or when shared with users.<\/strong><\/p>\n<p>\u2705 Correct:\u00a0<code>https:\/\/lieferando.at\/fr#cartes-\u00e9pargne<\/code><br \/>\u274c Wrong:\u00a0<code>https:\/\/lieferando.at\/fr#cartes-%C3%A9pargne<\/code><\/p>\n<p>However, when these URLs are processed by\u00a0<code>URLComponents<\/code>\u00a0in Swift, they may get encoded. The validator must handle both forms and compare them correctly:<\/p>\n<pre><code class=\"language-swift\">private func findMatch(for url: URL, in regEx: NSRegularExpression) -&gt; Bool {\n    let searchString = url.absoluteString.removingPercentEncoding!\n    let searchRange = NSRange(location: 0, length: searchString.utf16.count)\n    if let result = regEx.firstMatch(in: searchString, options: [.anchored], range: searchRange) {\n        return result.range.length == searchRange.length\n    }\n    return false\n}\n<\/code><\/pre>\n<p>The key is to decode the URL before matching against the regex.<\/p>\n<h2 id=\"putting-it-all-together-the-complete-validation-pipeline\">Putting It All Together: The Complete Validation Pipeline<\/h2>\n<p>To address all these challenges, I built\u00a0<strong>AASAValidator<\/strong>, a Swift command-line tool that provides three main commands:<\/p>\n<ol>\n<li><strong><code>validate-schema<\/code><\/strong>: Validates AASA files against a JSON schema<\/li>\n<li><strong><code>validate-cdn<\/code><\/strong>: Compares your website&#8217;s AASA file with Apple&#8217;s CDN version<\/li>\n<li><strong><code>validate-universal-links<\/code><\/strong>: Validates that specific URLs match (or don&#8217;t match) your AASA file patterns<\/li>\n<\/ol>\n<p>The tool handles:<\/p>\n<ul>\n<li>JSON schema validation<\/li>\n<li>CDN comparison<\/li>\n<li>Regular expression parsing and conversion<\/li>\n<li>Substitution variable expansion<\/li>\n<li>Complex URL matching with query parameters and fragments<\/li>\n<li>Exclusion validation<\/li>\n<li>Percent encoding<\/li>\n<\/ul>\n<h3 id=\"the-full-automation-pipeline\">The Full Automation Pipeline<\/h3>\n<h4 id=\"1-pr-validation-ci\">1. PR Validation (CI)<\/h4>\n<ul>\n<li><strong>Schema validation<\/strong>: Ensure AASA files are structurally correct<\/li>\n<li><strong>Content validation<\/strong>: Verify all expected URLs match (or don&#8217;t match) correctly<\/li>\n<li><strong>Bundle ID validation<\/strong>: Ensure the target app&#8217;s bundle ID is in the AASA<\/li>\n<\/ul>\n<h4 id=\"2-post-deployment-staging\">2. Post-Deployment (Staging)<\/h4>\n<ul>\n<li>Deploy AASA files to staging environment<\/li>\n<li>Test Universal Links on physical devices with staging builds<\/li>\n<li>Verify end-to-end behavior<\/li>\n<\/ul>\n<h4 id=\"3-post-deployment-production\">3. Post-Deployment (Production)<\/h4>\n<ul>\n<li>Deploy AASA files to production<\/li>\n<li><strong>CDN validation<\/strong>: Check that Apple&#8217;s CDN has the latest version<\/li>\n<li>Smoke test with production app<\/li>\n<\/ul>\n<h4 id=\"4-ongoing-monitoring\">4. Ongoing Monitoring<\/h4>\n<ul>\n<li>Daily CDN synchronization checks<\/li>\n<li>Alerting if files fall out of sync<\/li>\n<\/ul>\n<h3 id=\"example-github-actions-integration\">Example GitHub Actions Integration<\/h3>\n<p>Here&#8217;s how you might use the validator in a GitHub Actions workflow:<\/p>\n<pre><code class=\"language-yaml\">strategy:\n  fail-fast: false\n    matrix:\n      domain:\n        - just-eat.co.uk\n        - just-eat.es\n      bundle-id:\n        - com.eatch.mobileapp\n        - com.justeat.JUSTEAT\n        - com.takeaway.lu\n\nsteps:\n\n  - name: Validate AASA Schema\n    run: |\n      AASAValidator validate-schema \n        --aasa-path .\/${{ matrix.domain }}-aasa.json \n        --json-schema-path path\/to\/JSONSchema.json\n\n  - name: Validate Universal Links\n    run: |\n      AASAValidator validate-universal-links \n        --bundle-id ${{ matrix.bundle-id }} \n        --domain ${{ matrix.domain }} \n        --aasa-path .\/${{ matrix.domain }}-aasa.json \n        --universal-links-path .\/universal-links\/${{ matrix.domain }}.json<\/code><\/pre>\n<h2 id=\"key-takeaways\">Key Takeaways<\/h2>\n<ol>\n<li><strong>Validate against a schema<\/strong>: JSON parsing success doesn&#8217;t mean your AASA file is valid. Use JSON Schema validation in CI to catch structural errors early.<\/li>\n<li><strong>Don&#8217;t trust the CDN blindly<\/strong>: Always verify that Apple&#8217;s CDN has your latest AASA file. Implement automated daily checks.<\/li>\n<li><strong>Implement custom regex parsing<\/strong>: No existing tool handles Apple&#8217;s wildcard pattern syntax. Build your own matcher to validate URL matching.<\/li>\n<li><strong>Test substitution variables properly<\/strong>: No existing tool handles\u00a0<code>applinks.substitutionVariables<\/code>. Build your own matcher or use custom validation logic.<\/li>\n<li><strong>Implement proper staging<\/strong>: Set up real staging domains with HTTPS. Testing Universal Links requires real infrastructure.<\/li>\n<li><strong>Automate regression testing<\/strong>: Maintain lists of expected URLs and validate them automatically. Manual testing doesn&#8217;t scale.<\/li>\n<li><strong>Watch your encoding<\/strong>: Special characters must not be percent-encoded in Universal Links. Handle encoding carefully in your validation logic.<\/li>\n<li><strong>Order matters for exclusions<\/strong>: Components are evaluated in order. Place exclusion rules before broader patterns.<\/li>\n<li><strong>Plan for multi-language support<\/strong>: If your website supports multiple languages, your AASA file needs substitution variables to handle localized URLs.<\/li>\n<\/ol>\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>Universal Links are deceptively simple on the surface but require careful attention to detail in practice. The challenges we&#8217;ve discussed (schema validation, CDN synchronization, regex parsing, substitution variables, staging environments, comprehensive link validation, and encoding) are often ignored but are essential for a robust implementation.<\/p>\n<p>By building tooling that addresses these challenges and integrating validation into your development workflow, you can ensure that Universal Links work reliably for your users. The investment in proper validation and testing pays off by preventing production issues and giving you confidence when making changes to your AASA files.<\/p>\n<p>Consider implementing similar validation for your own Universal Links setup and your future self will thank you.<\/p>\n<\/p>\n<ul><\/ul>\n<\/div>\n<div class=\"pvc_clear\"><\/div>\n<p id=\"pvc_stats_1893\" class=\"pvc_stats total_only  \" data-element-id=\"1893\" style=\"\"><i class=\"pvc-stats-icon medium\" aria-hidden=\"true\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" version=\"1.0\" viewBox=\"0 0 502 315\" preserveAspectRatio=\"xMidYMid meet\"><g transform=\"translate(0,332) scale(0.1,-0.1)\" fill=\"\" stroke=\"none\"><path d=\"M2394 3279 l-29 -30 -3 -207 c-2 -182 0 -211 15 -242 39 -76 157 -76 196 0 15 31 17 60 15 243 l-3 209 -33 29 c-26 23 -41 29 -80 29 -41 0 -53 -5 -78 -31z\"\/><path d=\"M3085 3251 c-45 -19 -58 -50 -96 -229 -47 -217 -49 -260 -13 -295 52 -53 146 -42 177 20 16 31 87 366 87 410 0 70 -86 122 -155 94z\"\/><path d=\"M1751 3234 c-13 -9 -29 -31 -37 -50 -12 -29 -10 -49 21 -204 19 -94 39 -189 45 -210 14 -50 54 -80 110 -80 34 0 48 6 76 34 21 21 34 44 34 59 0 14 -18 113 -40 219 -37 178 -43 195 -70 221 -36 32 -101 37 -139 11z\"\/><path d=\"M1163 3073 c-36 -7 -73 -59 -73 -102 0 -56 133 -378 171 -413 34 -32 83 -37 129 -13 70 36 67 87 -16 290 -86 209 -89 214 -129 231 -35 14 -42 15 -82 7z\"\/><path d=\"M3689 3066 c-15 -9 -33 -30 -42 -48 -48 -103 -147 -355 -147 -375 0 -98 131 -148 192 -74 13 15 57 108 97 206 80 196 84 226 37 273 -30 30 -99 39 -137 18z\"\/><path d=\"M583 2784 c-38 -19 -67 -74 -58 -113 9 -42 211 -354 242 -373 16 -10 45 -18 66 -18 51 0 107 52 107 100 0 39 -1 41 -124 234 -80 126 -108 162 -133 173 -41 17 -61 16 -100 -3z\"\/><path d=\"M4250 2784 c-14 -9 -74 -91 -133 -183 -95 -150 -107 -173 -107 -213 0 -55 33 -94 87 -104 67 -13 90 8 211 198 130 202 137 225 78 284 -27 27 -42 34 -72 34 -22 0 -50 -8 -64 -16z\"\/><path d=\"M2275 2693 c-553 -48 -1095 -270 -1585 -649 -135 -104 -459 -423 -483 -476 -23 -49 -22 -139 2 -186 73 -142 361 -457 571 -626 285 -228 642 -407 990 -497 242 -63 336 -73 660 -74 310 0 370 5 595 52 535 111 1045 392 1455 803 122 121 250 273 275 326 19 41 19 137 0 174 -41 79 -309 363 -465 492 -447 370 -946 591 -1479 653 -113 14 -422 18 -536 8z m395 -428 c171 -34 330 -124 456 -258 112 -119 167 -219 211 -378 27 -96 24 -300 -5 -401 -72 -255 -236 -447 -474 -557 -132 -62 -201 -76 -368 -76 -167 0 -236 14 -368 76 -213 98 -373 271 -451 485 -162 444 86 934 547 1084 153 49 292 57 452 25z m909 -232 c222 -123 408 -262 593 -441 76 -74 138 -139 138 -144 0 -16 -233 -242 -330 -319 -155 -123 -309 -223 -461 -299 l-81 -41 32 46 c18 26 49 83 70 128 143 306 141 649 -6 957 -25 52 -61 116 -79 142 l-34 47 45 -20 c26 -10 76 -36 113 -56z m-2057 25 c-40 -58 -105 -190 -130 -263 -110 -324 -59 -707 132 -981 25 -35 42 -64 37 -64 -19 0 -241 119 -326 174 -188 122 -406 314 -532 468 l-58 71 108 103 c185 178 428 349 672 473 66 33 121 60 123 61 2 0 -10 -19 -26 -42z\"\/><path d=\"M2375 1950 c-198 -44 -350 -190 -395 -379 -18 -76 -8 -221 19 -290 114 -284 457 -406 731 -260 98 52 188 154 231 260 27 69 37 214 19 290 -38 163 -166 304 -326 360 -67 23 -215 33 -279 19z\"\/><\/g><\/svg><\/i> <img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"16\" height=\"16\" alt=\"Loading\" src=\"https:\/\/i0.wp.com\/dev95.site\/wp-content\/plugins\/page-views-count\/ajax-loader-2x.gif?resize=16%2C16&#038;ssl=1\" border=0 \/><\/p>\n<div class=\"pvc_clear\"><\/div>\n","protected":false},"excerpt":{"rendered":"<p>Originally published on the\u00a0Just Eat\u00a0Takeaway\u00a0Engineering Blog. Universal Links have been around since iOS 9 (2015), yet the topic remains surprisingly underrated in the iOS community. While most developers understand the basic concept (associating your website with your app so links<\/p>\n<div class=\"hosteria-entry-more\"><a href=\"https:\/\/dev95.site\/ar\/universal-links-at-scale-the-challenges-nobody-talks-about\/\" class=\"no-underline font-light  group-hover:text-primary-800 dark:group-hover:text-primary-300 py-1\">Read more &gt;&gt;&gt;<\/a><\/div>\n<div class=\"pvc_clear\"><\/div>\n<p id=\"pvc_stats_1893\" class=\"pvc_stats total_only\" data-element-id=\"1893\" style=\"\"><i class=\"pvc-stats-icon medium\" aria-hidden=\"true\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" version=\"1.0\" viewbox=\"0 0 502 315\" preserveaspectratio=\"xMidYMid meet\"><g transform=\"translate(0,332) scale(0.1,-0.1)\" fill=\"\" stroke=\"none\"><path d=\"M2394 3279 l-29 -30 -3 -207 c-2 -182 0 -211 15 -242 39 -76 157 -76 196 0 15 31 17 60 15 243 l-3 209 -33 29 c-26 23 -41 29 -80 29 -41 0 -53 -5 -78 -31z\"\/><path d=\"M3085 3251 c-45 -19 -58 -50 -96 -229 -47 -217 -49 -260 -13 -295 52 -53 146 -42 177 20 16 31 87 366 87 410 0 70 -86 122 -155 94z\"\/><path d=\"M1751 3234 c-13 -9 -29 -31 -37 -50 -12 -29 -10 -49 21 -204 19 -94 39 -189 45 -210 14 -50 54 -80 110 -80 34 0 48 6 76 34 21 21 34 44 34 59 0 14 -18 113 -40 219 -37 178 -43 195 -70 221 -36 32 -101 37 -139 11z\"\/><path d=\"M1163 3073 c-36 -7 -73 -59 -73 -102 0 -56 133 -378 171 -413 34 -32 83 -37 129 -13 70 36 67 87 -16 290 -86 209 -89 214 -129 231 -35 14 -42 15 -82 7z\"\/><path d=\"M3689 3066 c-15 -9 -33 -30 -42 -48 -48 -103 -147 -355 -147 -375 0 -98 131 -148 192 -74 13 15 57 108 97 206 80 196 84 226 37 273 -30 30 -99 39 -137 18z\"\/><path d=\"M583 2784 c-38 -19 -67 -74 -58 -113 9 -42 211 -354 242 -373 16 -10 45 -18 66 -18 51 0 107 52 107 100 0 39 -1 41 -124 234 -80 126 -108 162 -133 173 -41 17 -61 16 -100 -3z\"\/><path d=\"M4250 2784 c-14 -9 -74 -91 -133 -183 -95 -150 -107 -173 -107 -213 0 -55 33 -94 87 -104 67 -13 90 8 211 198 130 202 137 225 78 284 -27 27 -42 34 -72 34 -22 0 -50 -8 -64 -16z\"\/><path d=\"M2275 2693 c-553 -48 -1095 -270 -1585 -649 -135 -104 -459 -423 -483 -476 -23 -49 -22 -139 2 -186 73 -142 361 -457 571 -626 285 -228 642 -407 990 -497 242 -63 336 -73 660 -74 310 0 370 5 595 52 535 111 1045 392 1455 803 122 121 250 273 275 326 19 41 19 137 0 174 -41 79 -309 363 -465 492 -447 370 -946 591 -1479 653 -113 14 -422 18 -536 8z m395 -428 c171 -34 330 -124 456 -258 112 -119 167 -219 211 -378 27 -96 24 -300 -5 -401 -72 -255 -236 -447 -474 -557 -132 -62 -201 -76 -368 -76 -167 0 -236 14 -368 76 -213 98 -373 271 -451 485 -162 444 86 934 547 1084 153 49 292 57 452 25z m909 -232 c222 -123 408 -262 593 -441 76 -74 138 -139 138 -144 0 -16 -233 -242 -330 -319 -155 -123 -309 -223 -461 -299 l-81 -41 32 46 c18 26 49 83 70 128 143 306 141 649 -6 957 -25 52 -61 116 -79 142 l-34 47 45 -20 c26 -10 76 -36 113 -56z m-2057 25 c-40 -58 -105 -190 -130 -263 -110 -324 -59 -707 132 -981 25 -35 42 -64 37 -64 -19 0 -241 119 -326 174 -188 122 -406 314 -532 468 l-58 71 108 103 c185 178 428 349 672 473 66 33 121 60 123 61 2 0 -10 -19 -26 -42z\"\/><path d=\"M2375 1950 c-198 -44 -350 -190 -395 -379 -18 -76 -8 -221 19 -290 114 -284 457 -406 731 -260 98 52 188 154 231 260 27 69 37 214 19 290 -38 163 -166 304 -326 360 -67 23 -215 33 -279 19z\"\/><\/g><\/svg><\/i> <img loading=\"lazy\" decoding=\"async\" width=\"16\" height=\"16\" alt=\"Loading\" src=\"https:\/\/dev95.site\/wp-content\/plugins\/page-views-count\/ajax-loader-2x.gif\" border=\"0\" \/><\/p>\n<div class=\"pvc_clear\"><\/div>","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"fp_fajr_begins":"","fp_fajr_iqamah":"","fp_dhuhr_begins":"","fp_dhuhr_iqamah":"","fp_asr_begins":"","fp_asr_iqamah":"","fp_maghrib_begins":"","fp_maghrib_iqamah":"","fp_isha_begins":"","fp_isha_iqamah":"","fp_midnight":"","fp_midnight_name":"","fp_sunrise":"","fp_single_prayer_begins_title":"","fp_single_prayer_iqamah_title":"","fp_prayer_times_for_today":"","fp_hijra_date":"","fp_fajr_name":"","fp_dhuhr_name":"","fp_asr_name":"","fp_maghrib_name":"","fp_isha_name":"","fp_sunrise_name":"","fp_currentDate":"","fp_current_time":"","fp_current_title":"","fp_current_location":"","fp_masjid_name":"","fp_prayer_title":"","fp_next_prayer_iqamah_time":"","fp_next_prayer_iqamah_title":"","fp_next_prayer_begins_time":"","fp_next_prayer_begins_title":"","fp_next_prayer_title":"","_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[37],"tags":[],"class_list":["post-1893","post","type-post","status-publish","format-standard","hentry","category-posts"],"a3_pvc":{"activated":true,"total_views":0,"today_views":0},"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.5 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Universal Links At Scale: The Challenges Nobody Talks About - Dev95<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/dev95.site\/ar\/universal-links-at-scale-the-challenges-nobody-talks-about\/\" \/>\n<meta property=\"og:locale\" content=\"ar_AR\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Universal Links At Scale: The Challenges Nobody Talks About - Dev95\" \/>\n<meta property=\"og:description\" content=\"Originally published on the\u00a0Just Eat\u00a0Takeaway\u00a0Engineering Blog. Universal Links have been around since iOS 9 (2015), yet the topic remains surprisingly underrated in the iOS community. While most developers understand the basic concept (associating your website with your app so linksRead more &gt;&gt;&gt;\" \/>\n<meta property=\"og:url\" content=\"https:\/\/dev95.site\/ar\/universal-links-at-scale-the-challenges-nobody-talks-about\/\" \/>\n<meta property=\"og:site_name\" content=\"Dev95\" \/>\n<meta property=\"article:published_time\" content=\"2026-01-15T23:34:00+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2026\/01\/0_5RZ69ENMLrgyZwGV.webp\" \/>\n<meta name=\"author\" content=\"dev95\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"\u0643\u064f\u062a\u0628 \u0628\u0648\u0627\u0633\u0637\u0629\" \/>\n\t<meta name=\"twitter:data1\" content=\"dev95\" \/>\n\t<meta name=\"twitter:label2\" content=\"\u0648\u0642\u062a \u0627\u0644\u0642\u0631\u0627\u0621\u0629 \u0627\u0644\u0645\u064f\u0642\u062f\u0651\u0631\" \/>\n\t<meta name=\"twitter:data2\" content=\"16 \u062f\u0642\u064a\u0642\u0629\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/dev95.site\\\/universal-links-at-scale-the-challenges-nobody-talks-about\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/universal-links-at-scale-the-challenges-nobody-talks-about\\\/\"},\"author\":{\"name\":\"dev95\",\"@id\":\"https:\\\/\\\/dev95.site\\\/#\\\/schema\\\/person\\\/b807805ffe2916206b04d0938bce0298\"},\"headline\":\"Universal Links At Scale: The Challenges Nobody Talks About\",\"datePublished\":\"2026-01-15T23:34:00+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/universal-links-at-scale-the-challenges-nobody-talks-about\\\/\"},\"wordCount\":2572,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/universal-links-at-scale-the-challenges-nobody-talks-about\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/storage.ghost.io\\\/c\\\/ae\\\/f4\\\/aef4d625-32a2-417b-86f4-22c70a9b47a1\\\/content\\\/images\\\/2026\\\/01\\\/0_5RZ69ENMLrgyZwGV.webp\",\"articleSection\":[\"Posts\"],\"inLanguage\":\"ar\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/dev95.site\\\/universal-links-at-scale-the-challenges-nobody-talks-about\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/dev95.site\\\/universal-links-at-scale-the-challenges-nobody-talks-about\\\/\",\"url\":\"https:\\\/\\\/dev95.site\\\/universal-links-at-scale-the-challenges-nobody-talks-about\\\/\",\"name\":\"Universal Links At Scale: The Challenges Nobody Talks About - Dev95\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/universal-links-at-scale-the-challenges-nobody-talks-about\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/universal-links-at-scale-the-challenges-nobody-talks-about\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/storage.ghost.io\\\/c\\\/ae\\\/f4\\\/aef4d625-32a2-417b-86f4-22c70a9b47a1\\\/content\\\/images\\\/2026\\\/01\\\/0_5RZ69ENMLrgyZwGV.webp\",\"datePublished\":\"2026-01-15T23:34:00+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/universal-links-at-scale-the-challenges-nobody-talks-about\\\/#breadcrumb\"},\"inLanguage\":\"ar\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/dev95.site\\\/universal-links-at-scale-the-challenges-nobody-talks-about\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"ar\",\"@id\":\"https:\\\/\\\/dev95.site\\\/universal-links-at-scale-the-challenges-nobody-talks-about\\\/#primaryimage\",\"url\":\"https:\\\/\\\/storage.ghost.io\\\/c\\\/ae\\\/f4\\\/aef4d625-32a2-417b-86f4-22c70a9b47a1\\\/content\\\/images\\\/2026\\\/01\\\/0_5RZ69ENMLrgyZwGV.webp\",\"contentUrl\":\"https:\\\/\\\/storage.ghost.io\\\/c\\\/ae\\\/f4\\\/aef4d625-32a2-417b-86f4-22c70a9b47a1\\\/content\\\/images\\\/2026\\\/01\\\/0_5RZ69ENMLrgyZwGV.webp\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/dev95.site\\\/universal-links-at-scale-the-challenges-nobody-talks-about\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/dev95.site\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Universal Links At Scale: The Challenges Nobody Talks About\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/dev95.site\\\/#website\",\"url\":\"https:\\\/\\\/dev95.site\\\/\",\"name\":\"Dev95\",\"description\":\"A comprehensive platform for data and knowledge, delivering reliable content that meets the aspirations of readers and enthusiasts.\",\"publisher\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/dev95.site\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"ar\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/dev95.site\\\/#organization\",\"name\":\"Dev95\",\"url\":\"https:\\\/\\\/dev95.site\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"ar\",\"@id\":\"https:\\\/\\\/dev95.site\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/i0.wp.com\\\/dev95.site\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/rbrrbr-6.png?fit=512%2C512&ssl=1\",\"contentUrl\":\"https:\\\/\\\/i0.wp.com\\\/dev95.site\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/rbrrbr-6.png?fit=512%2C512&ssl=1\",\"width\":512,\"height\":512,\"caption\":\"Dev95\"},\"image\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/dev95.site\\\/#\\\/schema\\\/person\\\/b807805ffe2916206b04d0938bce0298\",\"name\":\"dev95\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"ar\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/a70a73d950838b20cd80d7ebdc955737e802e8cd896044c5473b32b946c0662a?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/a70a73d950838b20cd80d7ebdc955737e802e8cd896044c5473b32b946c0662a?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/a70a73d950838b20cd80d7ebdc955737e802e8cd896044c5473b32b946c0662a?s=96&d=mm&r=g\",\"caption\":\"dev95\"},\"url\":\"https:\\\/\\\/dev95.site\\\/ar\\\/author\\\/mohammad\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Universal Links At Scale: The Challenges Nobody Talks About - Dev95","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/dev95.site\/ar\/universal-links-at-scale-the-challenges-nobody-talks-about\/","og_locale":"ar_AR","og_type":"article","og_title":"Universal Links At Scale: The Challenges Nobody Talks About - Dev95","og_description":"Originally published on the\u00a0Just Eat\u00a0Takeaway\u00a0Engineering Blog. Universal Links have been around since iOS 9 (2015), yet the topic remains surprisingly underrated in the iOS community. While most developers understand the basic concept (associating your website with your app so linksRead more &gt;&gt;&gt;","og_url":"https:\/\/dev95.site\/ar\/universal-links-at-scale-the-challenges-nobody-talks-about\/","og_site_name":"Dev95","article_published_time":"2026-01-15T23:34:00+00:00","og_image":[{"url":"https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2026\/01\/0_5RZ69ENMLrgyZwGV.webp","type":"","width":"","height":""}],"author":"dev95","twitter_card":"summary_large_image","twitter_misc":{"\u0643\u064f\u062a\u0628 \u0628\u0648\u0627\u0633\u0637\u0629":"dev95","\u0648\u0642\u062a \u0627\u0644\u0642\u0631\u0627\u0621\u0629 \u0627\u0644\u0645\u064f\u0642\u062f\u0651\u0631":"16 \u062f\u0642\u064a\u0642\u0629"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/dev95.site\/universal-links-at-scale-the-challenges-nobody-talks-about\/#article","isPartOf":{"@id":"https:\/\/dev95.site\/universal-links-at-scale-the-challenges-nobody-talks-about\/"},"author":{"name":"dev95","@id":"https:\/\/dev95.site\/#\/schema\/person\/b807805ffe2916206b04d0938bce0298"},"headline":"Universal Links At Scale: The Challenges Nobody Talks About","datePublished":"2026-01-15T23:34:00+00:00","mainEntityOfPage":{"@id":"https:\/\/dev95.site\/universal-links-at-scale-the-challenges-nobody-talks-about\/"},"wordCount":2572,"commentCount":0,"publisher":{"@id":"https:\/\/dev95.site\/#organization"},"image":{"@id":"https:\/\/dev95.site\/universal-links-at-scale-the-challenges-nobody-talks-about\/#primaryimage"},"thumbnailUrl":"https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2026\/01\/0_5RZ69ENMLrgyZwGV.webp","articleSection":["Posts"],"inLanguage":"ar","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/dev95.site\/universal-links-at-scale-the-challenges-nobody-talks-about\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/dev95.site\/universal-links-at-scale-the-challenges-nobody-talks-about\/","url":"https:\/\/dev95.site\/universal-links-at-scale-the-challenges-nobody-talks-about\/","name":"Universal Links At Scale: The Challenges Nobody Talks About - Dev95","isPartOf":{"@id":"https:\/\/dev95.site\/#website"},"primaryImageOfPage":{"@id":"https:\/\/dev95.site\/universal-links-at-scale-the-challenges-nobody-talks-about\/#primaryimage"},"image":{"@id":"https:\/\/dev95.site\/universal-links-at-scale-the-challenges-nobody-talks-about\/#primaryimage"},"thumbnailUrl":"https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2026\/01\/0_5RZ69ENMLrgyZwGV.webp","datePublished":"2026-01-15T23:34:00+00:00","breadcrumb":{"@id":"https:\/\/dev95.site\/universal-links-at-scale-the-challenges-nobody-talks-about\/#breadcrumb"},"inLanguage":"ar","potentialAction":[{"@type":"ReadAction","target":["https:\/\/dev95.site\/universal-links-at-scale-the-challenges-nobody-talks-about\/"]}]},{"@type":"ImageObject","inLanguage":"ar","@id":"https:\/\/dev95.site\/universal-links-at-scale-the-challenges-nobody-talks-about\/#primaryimage","url":"https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2026\/01\/0_5RZ69ENMLrgyZwGV.webp","contentUrl":"https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2026\/01\/0_5RZ69ENMLrgyZwGV.webp"},{"@type":"BreadcrumbList","@id":"https:\/\/dev95.site\/universal-links-at-scale-the-challenges-nobody-talks-about\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/dev95.site\/"},{"@type":"ListItem","position":2,"name":"Universal Links At Scale: The Challenges Nobody Talks About"}]},{"@type":"WebSite","@id":"https:\/\/dev95.site\/#website","url":"https:\/\/dev95.site\/","name":"Dev95","description":"A comprehensive platform for data and knowledge, delivering reliable content that meets the aspirations of readers and enthusiasts.","publisher":{"@id":"https:\/\/dev95.site\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/dev95.site\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"ar"},{"@type":"Organization","@id":"https:\/\/dev95.site\/#organization","name":"Dev95","url":"https:\/\/dev95.site\/","logo":{"@type":"ImageObject","inLanguage":"ar","@id":"https:\/\/dev95.site\/#\/schema\/logo\/image\/","url":"https:\/\/i0.wp.com\/dev95.site\/wp-content\/uploads\/2026\/07\/rbrrbr-6.png?fit=512%2C512&ssl=1","contentUrl":"https:\/\/i0.wp.com\/dev95.site\/wp-content\/uploads\/2026\/07\/rbrrbr-6.png?fit=512%2C512&ssl=1","width":512,"height":512,"caption":"Dev95"},"image":{"@id":"https:\/\/dev95.site\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/dev95.site\/#\/schema\/person\/b807805ffe2916206b04d0938bce0298","name":"dev95","image":{"@type":"ImageObject","inLanguage":"ar","@id":"https:\/\/secure.gravatar.com\/avatar\/a70a73d950838b20cd80d7ebdc955737e802e8cd896044c5473b32b946c0662a?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/a70a73d950838b20cd80d7ebdc955737e802e8cd896044c5473b32b946c0662a?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/a70a73d950838b20cd80d7ebdc955737e802e8cd896044c5473b32b946c0662a?s=96&d=mm&r=g","caption":"dev95"},"url":"https:\/\/dev95.site\/ar\/author\/mohammad\/"}]}},"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/posts\/1893","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/comments?post=1893"}],"version-history":[{"count":0,"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/posts\/1893\/revisions"}],"wp:attachment":[{"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/media?parent=1893"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/categories?post=1893"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/tags?post=1893"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}