Deep linking w .NET MAUI 10: iOS Universal Links i Android App Links (przewodnik 2026)

Kompletny przewodnik po deep linkach w .NET MAUI 10: konfiguracja Universal Links na iOS, App Links na Androidzie, routing w Shell i najczęstsze błędy weryfikacji AASA oraz assetlinks.json. Z gotowym kodem, przykładami i checklistą do debugowania.

Deep Linking .NET MAUI 10 - Przewodnik 2026
On this page
  1. Czym są deep links, Universal Links i App Links?
  2. Porównanie: URL Schemes vs Universal Links vs App Links
  3. Konfiguracja iOS Universal Links krok po kroku
  4. Konfiguracja Android App Links krok po kroku
  5. Obsługa linków w kodzie .NET MAUI (Shell + routing)
  6. Jak testować deep linki na iOS i Androidzie?
  7. Najczęstsze błędy i jak je diagnozować
  8. Najczęściej zadawane pytania

Zaktualizowano: 11 września 2026

Deep linking w .NET MAUI 10 to mechanizm otwierania konkretnego ekranu aplikacji z zewnętrznego URL. Na iOS realizowany jest przez Universal Links (skojarzone domeny), a na Androidzie przez App Links (weryfikowane intencje HTTPS). W obu przypadkach potrzebujesz publicznie dostępnego pliku metadanych pod adresem /.well-known/, wpisu w konfiguracji projektu (Entitlements na iOS, intent-filter na Androidzie) oraz routingu w AppShell. W tym przewodniku pokażę dokładnie, co dzieje się pod maską, bo połowa poradników zatrzymuje się na Shell.Current.GoToAsync i zapomina o tym, że jedna literówka w apple-app-site-association potrafi zablokować deep linki na dwa tygodnie. Sam się o tym przekonałem, wypuszczając aplikację e-commerce w zeszłym roku.

Deep link to URL, który zamiast otwierać stronę w przeglądarce, prowadzi bezpośrednio do konkretnego widoku w aplikacji mobilnej. W ekosystemie natywnym istnieją trzy warianty tej idei: custom URL schemes (schemat myapp://profil/42), iOS Universal Links oraz Android App Links. Pierwsza wersja to relikt z ery iOS 8 i Androida 5. Działa, ale nie ma żadnej weryfikacji, więc dowolna inna aplikacja może zarejestrować ten sam schemat i przejąć ruch.

Universal Links (Apple, iOS 9+) oraz App Links (Google, Android 6+) rozwiązują problem przejęcia w ten sposób, że system operacyjny sam weryfikuje własność domeny za pomocą pliku podpisu na Twoim serwerze HTTPS. Gdy użytkownik kliknie link typu https://sklep.przyklad.pl/produkty/123, iOS i Android sprawdzają, czy w chwili instalacji Twojej aplikacji pobrany plik potwierdza, że to Ty jesteś prawnym właścicielem domeny. Dopiero wtedy link otwiera się w aplikacji, a nie w Safari czy Chrome.

W praktyce oznacza to, że deep linking to nie tylko funkcja UX. To także element bezpieczeństwa. Jeśli Twoja aplikacja obsługuje płatności, resetowanie hasła albo linki magic-login z e-maila, custom scheme po prostu nie wystarczy. Apple w dokumentacji Supporting associated domains otwarcie odradza używanie custom schemes do wrażliwych flow.

Zanim wybierzesz wariant, warto zobaczyć na jednym rzucie oka, co dostajesz w każdej z opcji. Poniższa tabela pochodzi z moich doświadczeń z aplikacjami produkcyjnymi, w których wspieramy oba systemy jednocześnie.

CechaCustom URL SchemeiOS Universal LinksAndroid App Links
Wymaga HTTPS?NieTak (ważny certyfikat)Tak (ważny certyfikat)
Weryfikacja własności domenyBrakapple-app-site-associationassetlinks.json + SHA-256
Ryzyko przejęcia linkuWysokieBrakBrak (przy autoVerify)
Fallback do przeglądarkiNieTakTak
Minimalna wersja OSiOS 2, Android 1iOS 9Android 6
Konfiguracja po stronie serweraBrakWymaganaWymagana
Wsparcie w .NET MAUI 10PełnePełne (Associated Domains)Pełne (intent-filter)
Rekomendowane w 2026?Tylko jako fallbackTak (produkcja)Tak (produkcja)

Wniosek jest prosty: w każdej nowej aplikacji domyślnym wyborem powinny być Universal Links i App Links z tego samego adresu HTTPS, a custom scheme trzymamy tylko dla ekstremalnych edge case'ów (np. połączenia międzyaplikacyjne w kontrolowanym ekosystemie firmowym). Jeśli planujesz też komunikację server-driven, warto to skoordynować z Twoją architekturą MVVM w .NET MAUI i nawigacją Shell, bo docelowo deep link musi trafić do dokładnie tego samego ViewModel, do którego trafiłby normalny push nawigacyjny.

Zacznij od pliku apple-app-site-association. To zwykły JSON serwowany pod adresem https://twojadomena.pl/.well-known/apple-app-site-association. Kluczowe rzeczy: brak rozszerzenia .json, Content-Type: application/json i odpowiedź bez przekierowań (301/302). Apple pobiera ten plik przy instalacji aplikacji oraz (od iOS 14) przez usługę CDN Apple, więc lokalne pliki hosts nie pomogą w testach na urządzeniu.

{
  "applinks": {
    "details": [
      {
        "appIDs": ["ABCDE12345.pl.przyklad.sklep"],
        "components": [
          {
            "/": "/produkty/*",
            "comment": "Otwiera szczegóły produktu"
          },
          {
            "/": "/koszyk",
            "comment": "Otwiera koszyk"
          }
        ]
      }
    ]
  }
}

Wartość appIDs to TeamID.BundleID. TeamID znajdziesz w portalu Apple Developer Account pod nagłówkiem Membership. Bundle ID to identyfikator z pliku Info.plist lub z <ApplicationId> w csproj.

Następnie edytuj Platforms/iOS/Entitlements.plist i dodaj:

<key>com.apple.developer.associated-domains</key>
<array>
  <string>applinks:twojadomena.pl</string>
</array>

W pliku csproj upewnij się, że w konfiguracji Release dla iOS masz <CodesignEntitlements>Platforms\iOS\Entitlements.plist</CodesignEntitlements>. To krok, o którym zapomina połowa deweloperów migrujących z Xamarin.Forms. Tam entitlements były w innym miejscu i nie zawsze przenoszą się automatycznie. Jeśli akurat robisz taką migrację, pomocny będzie osobny przewodnik po migracji Xamarin.Forms do .NET MAUI 10, gdzie omawiam pułapki związane z konfiguracją projektową.

Android używa pliku assetlinks.json pod adresem https://twojadomena.pl/.well-known/assetlinks.json. Format opisuje standard Digital Asset Links dla Android App Links. Kluczowa jest sekcja sha256_cert_fingerprints, czyli odcisk klucza podpisującego wersję Release. Nie może to być klucz debug ani upload key z Play App Signing.

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "pl.przyklad.sklep",
    "sha256_cert_fingerprints": [
      "14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"
    ]
  }
}]

Jak zdobyć SHA-256? Jeśli używasz Play App Signing (a od 2021 Google to praktycznie wymóg dla nowych aplikacji), znajdziesz odcisk w Google Play Console → App Integrity → App signing key certificate. Warto tam sprawdzić oba odciski: upload i signing. Dla developmentu potrzebujesz upload key, ale dla produkcji zawsze signing key. Możesz spokojnie dodać oba do tablicy sha256_cert_fingerprints.

Teraz w Platforms/Android/AndroidManifest.xml dodaj do głównej Activity intent-filter:

<activity ...>
  <intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="https"
          android:host="twojadomena.pl" />
  </intent-filter>
</activity>

Atrybut android:autoVerify="true" to sedno mechanizmu. Mówi systemowi, że przy instalacji aplikacji ma automatycznie pobrać assetlinks.json i zweryfikować podpis. Bez tego atrybutu Android otworzy chooser ("Otwórz w:") zamiast bezpośrednio Twojej aplikacji. Od Androida 12 (API 31) domyślnie tylko zweryfikowane linki otwierają aplikację automatycznie, więc autoVerify nie jest już opcjonalne.

Obsługa linków w kodzie .NET MAUI (Shell + routing)

Konfiguracja platformowa to połowa roboty. Druga połowa to routing wewnątrz aplikacji. W .NET MAUI 10 rekomendowanym podejściem jest Shell z rejestracją tras. Zacznijmy od zarejestrowania tras w konstruktorze AppShell:

public partial class AppShell : Shell
{
    public AppShell()
    {
        InitializeComponent();
        Routing.RegisterRoute("produkty/szczegoly", typeof(ProductDetailPage));
        Routing.RegisterRoute("koszyk", typeof(CartPage));
    }
}

Teraz w App.xaml.cs nadpisz metodę OnAppLinkRequestReceived, która jest wywoływana zarówno przy Universal Link (iOS), jak i App Link (Android):

protected override async void OnAppLinkRequestReceived(Uri uri)
{
    base.OnAppLinkRequestReceived(uri);

    if (uri.Host != "twojadomena.pl")
        return;

    var segments = uri.AbsolutePath.Trim('/').Split('/');

    if (segments.Length == 2 && segments[0] == "produkty")
    {
        var productId = segments[1];
        await Shell.Current.GoToAsync(
            $"//produkty/szczegoly?id={Uri.EscapeDataString(productId)}");
    }
    else if (segments.Length == 1 && segments[0] == "koszyk")
    {
        await Shell.Current.GoToAsync("//koszyk");
    }
}

Docelowa ProductDetailPage odbiera parametr przez atrybut QueryPropertyAttribute na kodzie code-behind albo (czystsze rozwiązanie) na ViewModelu wstrzykniętym przez DI. Podwójny slash // w ścieżce resetuje stos nawigacji do korzenia, co jest zwykle pożądane przy wejściu z linku zewnętrznego. Jeśli chcesz zachować bieżący kontekst (np. użytkownik był już w aplikacji), użyj pojedynczego /.

W .NET MAUI 10 dodano też natywne wsparcie dla WebAuthenticator callbacks przez Universal Links, co eliminuje osobny custom scheme dla OAuth. Jeśli robisz uwierzytelnianie, przy okazji zerknij, jak łączyć to z bezpieczeństwem aplikacji .NET MAUI i SecureStorage. Token przechowuj w Keychain/Keystore, a nie w preferences.

Jak testować deep linki na iOS i Androidzie?

Testowanie deep linków to obszar, w którym najwięcej deweloperów się potyka, bo klikanie w link z Notatek albo z Safari zachowuje się inaczej niż otwarcie przez terminal. Honestly, sam wpadam w tę pułapkę co kilka miesięcy. Poniżej zestaw komend, których używam codziennie.

iOS, symulator

# Otwórz Universal Link w symulatorze
xcrun simctl openurl booted https://twojadomena.pl/produkty/123

# Sprawdź, czy plik AASA jest poprawnie pobrany
xcrun simctl spawn booted swcutil dl

iOS, urządzenie fizyczne

Na urządzeniu użyj Apple's App Search API Validation Tool. Wpisujesz URL i widzisz, jak Apple pobiera i parsuje Twój plik. Jeśli plik ma błąd składni JSON albo zły Content-Type, tool od razu to pokaże. Alternatywnie, po zainstalowaniu aplikacji przez Xcode włącz w Settings → Developer → Universal Links → Diagnostics, żeby zobaczyć status per domena.

Android, emulator i urządzenie

# Otwórz App Link
adb shell am start -a android.intent.action.VIEW \
    -c android.intent.category.BROWSABLE \
    -d "https://twojadomena.pl/produkty/123"

# Sprawdź status weryfikacji dla Twojej aplikacji
adb shell pm get-app-links pl.przyklad.sklep

# Wymuś ponowną weryfikację (po zmianie assetlinks.json)
adb shell pm verify-app-links --re-verify pl.przyklad.sklep

Dla pełnej pewności, że testy end-to-end nie zepsują się przy kolejnym wydaniu, warto pokryć deep linki testami UI. Jak to zorganizować w praktyce, opisałem w przewodniku o testowaniu aplikacji .NET MAUI z Appium i xUnit. Appium potrafi wystartować aplikację z konkretnym URI capability, co świetnie nadaje się do smoke testów deep linków w CI.

Najczęstsze błędy i jak je diagnozować

Po siedmiu latach shipowania aplikacji z deep linkami mam listę pułapek, w które wpadają praktycznie wszyscy. Oto pięć najczęstszych. Trafiłem na każdą z nich osobiście, więc mówię z doświadczenia, nie z lektury dokumentacji.

1. Serwer zwraca 301 przy pobieraniu AASA

Apple nie podąża za przekierowaniami przy pobieraniu apple-app-site-association. Jeśli Twój CDN robi redirect z http:// na https:// albo z www na apex, plik nie zostanie pobrany. Rozwiązanie: dodaj domenę bez www do Associated Domains i skonfiguruj serwer tak, żeby /.well-known/apple-app-site-association był serwowany bezpośrednio ze statusem 200.

2. Zły Content-Type dla AASA

Plik musi być serwowany jako application/json. Nginx często domyślnie serwuje pliki bez rozszerzenia jako application/octet-stream. Fix w Nginx:

location = /.well-known/apple-app-site-association {
    default_type application/json;
}

3. SHA-256 z debug key zamiast release key na Androidzie

Klasyk. Deweloper testuje z Android Studio (debug key), wszystko działa, po opublikowaniu w Google Play przestaje. Google Play używa własnego signing key (Play App Signing) i to jego odcisk musi być w assetlinks.json. Dodaj oba: upload key i signing key.

4. Brak weryfikacji na Androidzie 12+

Od Androida 12 użytkownik musi ręcznie potwierdzić otwieranie linków w aplikacji, jeśli weryfikacja się nie powiodła. Bez autoVerify="true" i poprawnego assetlinks.json nawet po instalacji z Play link otworzy się w Chrome. Zawsze sprawdzaj pm get-app-links po pierwszej instalacji.

Ten problem dotyczy głównie .NET MAUI. Jeśli w OnAppLinkRequestReceived używasz Shell.Current.GoToAsync("//sciezka"), resetujesz stos nawigacji. Jeśli aplikacja była w tle w środku innego flow (np. checkout), użytkownik straci kontekst. Rozważ trzymanie pending navigation w statycznym property i wykonanie go po zakończeniu obecnej akcji użytkownika.

Najczęściej zadawane pytania

Tak, Apple wymaga ważnego certyfikatu HTTPS dla domeny serwującej apple-app-site-association. Certyfikat self-signed nie zadziała nawet w developmencie. Let's Encrypt jest w zupełności wystarczający dla produkcji.

Deep link to ogólne pojęcie dla URL prowadzącego do konkretnego widoku w aplikacji. Universal Link to konkretna implementacja Apple używająca zwykłego HTTPS URL i weryfikacji własności domeny. Custom scheme (myapp://) to też deep link, ale bez weryfikacji.

Czy jeden URL może obsłużyć iOS i Android?

Tak, i to jest największa zaleta Universal Links plus App Links. Ten sam URL https://twojadomena.pl/produkty/123 otworzy aplikację na iOS, aplikację na Androidzie, a jeśli aplikacja nie jest zainstalowana, stronę WWW. Potrzebujesz obu plików (apple-app-site-association i assetlinks.json) pod /.well-known/.

Prawdopodobnie weryfikacja się nie powiodła. Uruchom adb shell pm get-app-links <package>. Jeśli status to none lub error, sprawdź: (1) czy assetlinks.json jest dostępny publicznie po HTTPS, (2) czy SHA-256 zgadza się z kluczem podpisującym APK, (3) czy w manifeście masz android:autoVerify="true".

Czy custom URL schemes są nadal wspierane w .NET MAUI 10?

Tak, w pełni. Custom scheme jest przydatny w izolowanych ekosystemach (kioski, aplikacje wewnętrzne firm) oraz jako fallback dla starszych systemów. W .NET MAUI 10 konfigurujesz go przez CFBundleURLTypes w Info.plist (iOS) i <data android:scheme="myapp" /> w intent-filter (Android). Nie używaj do flow wrażliwych na bezpieczeństwo.

Na iOS: użyj TestFlight lub Ad Hoc distribution, Universal Links działają identycznie. Na Androidzie: zbuduj wersję Release podpisaną tym samym kluczem co produkcja, zainstaluj przez ADB, potem adb shell am start -a android.intent.action.VIEW -d URL. Nie testuj Universal Links w Safari, Apple blokuje otwieranie aplikacji z tej samej domeny po kliknięciu w Safari.