Universal Links a lo grande: del fichero AASA a una infraestructura fiable
Arturo Rivas Arias
Los Universal Links suelen presentarse como una funcionalidad sencilla: se añade la capability de Associated Domains al proyecto, se publica un fichero apple-app-site-association en el servidor y la aplicación procesa la URL recibida. Esta explicación es correcta, pero solo describe el camino ideal.
Cuando una aplicación trabaja con varios dominios, distintas versiones, entornos de pruebas, rutas localizadas por idioma o campañas de marketing, ese pequeño fichero JSON se convierte en parte de la infraestructura del producto. Un error no produce necesariamente un crash ni una respuesta HTTP fallida. El enlace, sencillamente, se abre en el navegador y la causa puede encontrarse en la aplicación, el servidor, la caché de Apple o una regla de redirección demasiado amplia.
Por eso, implementar los enlaces universales con configuraciones complejas exige tratarlos como un contrato entre tres sistemas: el sitio web declara qué aplicaciones pueden gestionar cada URL, el binario declara qué dominios acepta y el código de la aplicación decide qué pantalla representa la URL recibida.
Por qué usar Universal Links
Un Universal Link es una URL https normal. Si la aplicación compatible está instalada, el sistema puede abrirla directamente; si no lo está, la misma dirección continúa funcionando en la web. Frente a los esquemas personalizados como myapp://, ofrece una asociación verificable con el dominio y evita que otra aplicación reclame arbitrariamente el mismo esquema.
La relación de confianza es bidireccional:
- La aplicación incluye dominios como
applinks:museo.exampleen el entitlementcom.apple.developer.associated-domains. - Cada dominio publica un fichero
apple-app-site-association, habitualmente denominado AASA, con los identificadores de las aplicaciones autorizadas y las rutas que pueden abrir. - La aplicación recibe la URL y la transforma en una navegación válida.
Si cualquiera de esas piezas no coincide exactamente con las demás, la asociación queda incompleta.
La configuración mínima
En Xcode, la capability Associated Domains genera una entrada similar a esta en el fichero de entitlements:
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:museo.example</string>
<string>applinks:entradas.museo.example</string>
</array>
El comodín de subdominios requiere nuestra atención. applinks:*.museo.example cubre entradas.museo.example, pero no el dominio raíz museo.example. Si la aplicación necesita ambos, debe declarar ambas entradas.
En el servidor, el fichero puede publicarse en:
https://museo.example/.well-known/apple-app-site-association
No lleva extensión .json, debe estar disponible mediante HTTPS y no debe responder con una redirección. Un ejemplo con la sintaxis moderna basada en components sería:
{
"applinks": {
"details": [
{
"appIDs": [
"ABCDE12345.com.example.museum"
],
"components": [
{
"/": "/admin/*",
"exclude": true,
"comment": "La administración permanece en la web"
},
{
"/": "/exhibitions/*",
"comment": "Abre el detalle de una exposición"
},
{
"/": "/tickets/*",
"?": {
"source": "*"
}
}
]
}
]
}
}
Las reglas se evalúan en orden y la primera coincidencia decide el resultado. Por este motivo, una exclusión específica debe aparecer antes que una regla positiva más general. Si /* estuviera en primer lugar, una exclusión posterior como /admin/* no llegaría a evaluarse.
La sintaxis components también permite aplicar condiciones al path mediante /, a los parámetros de consulta con ? y al fragmento con #. Para que un diccionario coincida, deben cumplirse todos los componentes que declara; los componentes omitidos se ignoran.
Un JSON válido no implica un AASA válido
Comprobar que el documento puede decodificarse como JSON solo detecta errores sintácticos. No revela que appIDs se haya escrito como un String, que falte details, que una propiedad tenga un nombre incorrecto o que el tipo de components no sea el esperado.
El AASA debería disponer de un JSON Schema propio y validarse en cada pull request. La comprobación puede incluir:
- Campos obligatorios y tipos correctos.
- Formato de cada App ID, compuesto por el App ID Prefix y el bundle identifier.
- Ausencia de claves desconocidas, cuando el esquema del proyecto sea estricto.
- Límite de tamaño del fichero.
- Reglas incompatibles o duplicadas.
- Presencia de todos los bundles de producción que deban compartir el dominio.
Conviene no convertir el esquema en una interpretación inventada de la plataforma. Apple mantiene una sintaxis antigua basada en appID y paths, además de la más expresiva basada en appIDs y components. Si el producto todavía admite versiones antiguas del sistema, el contrato y los tests tienen que reflejarlo.
la CDN de Apple introduce una segunda fuente de verdad
Desde iOS 14 y macOS 11, los dispositivos obtienen normalmente los ficheros de asociación mediante un CDN gestionado por Apple. Por tanto, que el AASA correcto aparezca al abrir la URL del dominio no demuestra que los dispositivos estén utilizando esa misma versión.
La copia que conserva Apple puede consultarse con una URL de esta forma:
https://app-site-association.cdn-apple.com/a/v1/museo.example
Después de un despliegue existen, al menos, dos estados que comprobar:
- El origen sirve el nuevo fichero con HTTPS, un código satisfactorio y el tipo de contenido esperado.
- La CDN de Apple ha obtenido una copia equivalente.
La propagación no es instantánea ni ofrece un mecanismo público para invalidar la caché a voluntad. Publicar una corrección urgente puede no reparar inmediatamente los dispositivos afectados. Una operación fiable debe comparar periódicamente el contenido del origen con el del CDN y generar una alerta si la diferencia persiste.
Comparar los bytes sin procesar puede provocar falsos positivos por cambios irrelevantes de formato. Es preferible decodificar ambos documentos y comparar su representación estructural o una versión canónica del JSON. Aun así, la monitorización no sustituye a una prueba real: solo confirma que Apple distribuye el contrato esperado.
Durante el desarrollo se puede añadir ?mode=developer al dominio asociado:
applinks:staging.museo.example?mode=developer
Con Associated Domains Development activado en los ajustes de desarrollador del dispositivo, este modo permite consultar directamente el servidor y evitar el CDN. Debe reservarse para builds de desarrollo; no representa el comportamiento de una build distribuida en producción.
Los patrones del AASA no son expresiones regulares
Los comodines * y ? pueden parecer una expresión regular, pero pertenecen a la sintaxis de asociación definida por Apple. Si se construye un validador interno, no basta con sustituir caracteres y ya está.
Antes de convertir un patrón hay que “escapar” los caracteres que sí tienen significado para el motor de expresiones regulares, expandir las variables de sustitución y conservar la semántica de componentes de la URL. De lo contrario, un punto, un paréntesis o un signo + procedente de una ruta real alterará el patrón resultante.
Las substitutionVariables ayudan a reducir duplicaciones cuando una ruta cambia según el idioma:
{
"applinks": {
"substitutionVariables": {
"collection": [
"coleccion",
"collection",
"collection-permanente"
]
},
"details": [
{
"appIDs": [
"ABCDE12345.com.example.museum"
],
"components": [
{
"/": "/$(lang)/$(collection)/*",
"percentEncoded": false
}
]
}
]
}
}
Apple proporciona variables predefinidas, entre ellas $(lang) y $(region), y permite declarar otras propias. En este ejemplo, una única regla representa rutas como /es/coleccion/modernismo y /en/collection/modernism.
El tratamiento del percent encoding debe ser explícito y coherente con percentEncoded. Decodificar siempre la URL antes de compararla, sin tener en cuenta esta propiedad, puede unir direcciones que no deberían considerarse equivalentes o cambiar el significado de caracteres reservados. Es mejor separar path, query y fragment con URLComponents y aplicar a cada componente la semántica que declara el AASA.
Las pruebas deben describir el comportamiento
Una lista de URLs de ejemplo junto al AASA funciona como documentación ejecutable. No solo deben probarse direcciones que abren la aplicación, sino también aquellas que deben permanecer en la web.
{
"opens_app": [
"https://museo.example/es/coleccion/modernismo",
"https://museo.example/exhibitions/photography",
"https://museo.example/tickets/annual?source=newsletter"
],
"opens_web": [
"https://museo.example/admin/reports",
"https://museo.example/legal/privacy",
"https://museo.example/tickets/annual"
]
}
El último caso es especialmente útil: la regla del ejemplo exige un parámetro source, así que la misma ruta sin él no debería coincidir. Cada modificación del AASA debe ejecutar esta matriz contra todas las aplicaciones y dominios afectados.
Estas pruebas permiten detectar tres regresiones diferentes:
- Una URL admitida ya no coincide con ninguna regla.
- Una URL admitida cae en una exclusión anterior.
- Una URL reservada para la web empieza a coincidir con una regla demasiado amplia.
En instalaciones con numerosos dominios, la matriz también debería comprobar que cada host publica el fichero correcto. Dos dominios similares pueden apuntar a distribuciones distintas, servir contenido diferente o quedar desincronizados aunque compartan repositorio.
Staging necesita dominios y certificados reales
La asociación forma parte de la seguridad de la plataforma, por lo que un fichero local no reproduce el comportamiento completo. Un entorno de staging fiable necesita un dominio accesible, HTTPS válido y una configuración AASA equivalente a producción.
Las builds de desarrollo pueden declarar dominios específicos de staging sin exponerlos en la configuración de distribución. El flujo recomendado incluye:
- Validar el esquema y la matriz de URLs en CI.
- Desplegar el AASA en staging.
- Comprobar cabeceras, ausencia de redirecciones y contenido publicado.
- Probar la apertura en un dispositivo físico con una build de staging.
- Desplegar en producción y esperar la propagación al CDN.
- Ejecutar una prueba de humo con la build distribuida.
El simulador resulta útil para el enrutado de la aplicación, pero no debería ser la única verificación de la asociación completa. También hay que recordar que TestFlight representa una distribución y puede usar entitlements distintos de una build Debug.
El AASA decide si se abre la app; Swift decide adónde ir
Aunque iOS determine que una URL pertenece a la aplicación, el código debe validarla de nuevo antes de navegar. Una ruta que el AASA admite puede estar incompleta, contener un identificador inválido o pertenecer a una funcionalidad que esa versión todavía no soporta.
En SwiftUI se puede centralizar la transformación de URLs en destinos de navegación:
import SwiftUI
enum MuseumDestination: Hashable {
case exhibition(slug: String)
case ticket(id: String, source: String?)
}
struct MuseumLinkParser {
private let allowedHosts = [
"museo.example",
"entradas.museo.example"
]
func destination(for url: URL) -> MuseumDestination? {
guard
url.scheme == "https",
let host = url.host(),
allowedHosts.contains(host)
else {
return nil
}
let components = url.pathComponents.filter { $0 != "/" }
switch components {
case ["exhibitions", let slug] where !slug.isEmpty:
return .exhibition(slug: slug)
case ["tickets", let id] where !id.isEmpty:
let queryItems = URLComponents(
url: url,
resolvingAgainstBaseURL: false
)?.queryItems
let source = queryItems?
.first(where: { $0.name == "source" })?
.value
return .ticket(id: id, source: source)
default:
return nil
}
}
}
La vista raíz recibe el enlace con onOpenURL y solo modifica la navegación cuando el parser devuelve un destino conocido:
struct MuseumAppView: View {
@State private var path: [MuseumDestination] = []
private let parser = MuseumLinkParser()
var body: some View {
NavigationStack(path: $path) {
HomeView()
.navigationDestination(for: MuseumDestination.self) { destination in
switch destination {
case .exhibition(let slug):
ExhibitionView(slug: slug)
case .ticket(let id, let source):
TicketView(id: id, campaignSource: source)
}
}
}
.onOpenURL { url in
guard let destination = parser.destination(for: url) else {
return
}
path = [destination]
}
}
}
Separar el parseo y navegación facilita probar el comportamiento sin lanzar la interfaz. Además, evita repartir comparaciones de cadenas con String entre vistas y ofrece un lugar único para validar hosts, segmentos, parámetros y compatibilidad entre versiones.
Diagnosticar por capas
Cuando un enlace no abre la aplicación, cambiar el AASA a ciegas suele empeorar el diagnóstico. Es más eficaz comprobar la cadena en orden:
- Binario: inspeccionar los entitlements firmados y confirmar el dominio exacto.
- Origen: verificar la URL del AASA, el certificado, el código HTTP, las cabeceras, el tamaño y la ausencia de redirecciones.
- Contenido: validar estructura, App ID, orden de reglas y resultado esperado para la URL concreta.
- CDN: comprobar qué versión está sirviendo Apple.
- Dispositivo: utilizar Universal Links Diagnostics en Ajustes > Developer y revisar los logs del subsistema
swcdcuando sea necesario. - Aplicación: confirmar que la URL llega al proceso y que el parser genera un destino soportado.
- Contexto: probar el enlace desde Mail, Mensajes o Notas, no escribiéndolo manualmente en la barra de Safari.
Existe además un comportamiento que puede confundirse con un fallo: si el usuario elige continuar en la web, iOS puede recordar esa preferencia. Del mismo modo, un enlace al mismo dominio que la página actualmente abierta en Safari puede permanecer en Safari para respetar la intención de navegación del usuario.
De configuración estática a contrato verificable
El fichero AASA no debería editarse como un recurso aislado subido manualmente a un servidor. En un sistema maduro forma una unidad con su esquema, una matriz de URLs, tests del parser Swift, entitlements por entorno y monitorización del CDN.
La automatización más útil combina cuatro momentos: validación antes de integrar el cambio, prueba end-to-end en staging, comprobación después del despliegue y vigilancia continua de la copia distribuida por Apple. Así, una nueva ruta deja de ser una modificación difícil de observar y pasa a ser un cambio revisable, probado y reversible.
Los Universal Links son fiables cuando la organización deja de tratarlos como una capability de Xcode y empieza a gestionarlos como una interfaz pública compartida entre web, infraestructura y aplicación.