smithery/evanbacon

universal-links

Set up and test universal links for iOS using setup-safari

Installation

$ npx skills add smithery/evanbacon --skill universal-links

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery/evanbacon.

npx skills add smithery/evanbacon

Browse all from smithery/evanbacon

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 5,623 B
  • docs SUMMARY.md 81 B

History

  1. First recorded snapshot · 0 installs

SKILL.md

Use bunx setup-safari to configure and test universal links (deep links) on iOS.

Important: When Custom Builds Are Needed

Universal links with custom domains require custom native builds. Your app will no longer work in Expo Go.

However, for basic deep linking during development:

  • Expo Go supports the exp:// URL scheme and Expo-hosted URLs
  • Custom builds required for your own domain's universal links (applinks:yourdomain.com)

If you only need deep linking for development/testing, try npx expo start with Expo Go first. Only create custom builds when you need production universal links with your own domain.

Automated Setup (Recommended)

Run setup-safari with your Apple ID to automate credential lookup:

EXPO_APPLE_ID="[email protected]" bunx setup-safari

This will:

  1. Authenticate with Apple Developer Portal
  2. Enable Associated Domains for your bundle ID
  3. Output the AASA file content and meta tag

After running, you must manually:

1. Create the AASA file

Create public/.well-known/apple-app-site-association (no file extension):

{
  "applinks": {
    "details": [
      {
        "appIDs": ["TEAM_ID.com.your.bundleid"],
        "components": [
          {
            "/": "*",
            "comment": "Matches all routes"
          }
        ]
      }
    ]
  },
  "activitycontinuation": {
    "apps": ["TEAM_ID.com.your.bundleid"]
  },
  "webcredentials": {
    "apps": ["TEAM_ID.com.your.bundleid"]
  }
}

2. Create src/app/+html.tsx with Smart App Banner

import { ScrollViewStyleReset } from "expo-router/html";

export default function Root({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta httpEquiv="X-UA-Compatible" content="IE=edge" />
        <meta
          name="viewport"
          content="width=device-width, initial-scale=1, shrink-to-fit=no"
        />
        <meta name="apple-itunes-app" content="app-id=YOUR_ITUNES_ID" />
        <ScrollViewStyleReset />
      </head>
      <body>{children}</body>
    </html>
  );
}

3. Add Associated Domains to app.json

{
  "expo": {
    "ios": {
      "associatedDomains": [
        "applinks:yourdomain.com",
        "activitycontinuation:yourdomain.com",
        "webcredentials:yourdomain.com"
      ]
    }
  }
}

4. Deploy and Rebuild

# Deploy web (AASA file must be accessible)
npx expo export -p web && npx eas-cli deploy

# Rebuild iOS app with new entitlements
npx expo run:ios
# Or for TestFlight: npx testflight

Interactive Setup (Alternative)

For interactive mode (requires TTY):

bunx setup-safari

How Universal Links Work

Universal links require two parts:

  1. AASA file on your server - Tells iOS which paths your app handles
  2. Associated Domains entitlement - Tells your app which domains to claim

AASA File Format

Host at https://yourdomain.com/.well-known/apple-app-site-association:

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "TEAM_ID.com.example.app",
        "paths": ["*"]
      }
    ]
  }
}

Path patterns:

  • * - Match all paths
  • /products/* - Match paths starting with /products/
  • /item/??? - Match exactly 3 characters after /item/
  • NOT /admin/* - Exclude paths

Associated Domains Entitlement

In your app.json or app.config.js:

{
  "expo": {
    "ios": {
      "associatedDomains": [
        "applinks:yourdomain.com",
        "applinks:www.yourdomain.com"
      ]
    }
  }
}

Testing Universal Links

After setup, test with setup-safari:

npx setup-safari

Or manually test in Safari:

  1. Open Safari on iOS device/simulator
  2. Navigate to a URL that should open your app
  3. Pull down to reveal the banner, or long-press the link

Testing with Tunnel (No Server Required)

Test universal links without deploying a website using Expo's tunnel feature:

  1. Set a consistent tunnel subdomain:
export EXPO_TUNNEL_SUBDOMAIN=my-app-name
  1. Configure associated domains with the ngrok URL:
{
  "expo": {
    "ios": {
      "associatedDomains": ["applinks:my-app-name.ngrok.io"]
    }
  }
}
  1. Build the development client:
npx expo run:ios
  1. Start the dev server with tunnel:
npx expo start --tunnel
  1. Test the link - Type https://my-app-name.ngrok.io in Safari on your device. It should open your app directly.

The tunnel creates a public HTTPS URL that serves the AASA file automatically, letting you test the full universal links flow during development.

Debugging

Check if Apple has cached your AASA:

curl "https://app-site-association.cdn-apple.com/a/v1/yourdomain.com"

Validate AASA file format:

curl https://yourdomain.com/.well-known/apple-app-site-association | jq

Common issues:

  • AASA must be served with Content-Type: application/json
  • HTTPS required (no self-signed certs)
  • Apple caches AASA files - changes may take time to propagate

Expo Router Integration

With Expo Router, handle incoming links automatically:

// app/[...path].tsx handles all deep link paths
// app/products/[id].tsx handles /products/:id links

Access link parameters:

import { useLocalSearchParams } from "expo-router";

export default function Product() {
  const { id } = useLocalSearchParams();
  return <Text>Product: {id}</Text>;
}