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.
Czym są deep links, Universal Links i App Links?
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.
Porównanie: URL Schemes vs Universal Links vs App Links
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.
| Cecha | Custom URL Scheme | iOS Universal Links | Android App Links |
|---|---|---|---|
| Wymaga HTTPS? | Nie | Tak (ważny certyfikat) | Tak (ważny certyfikat) |
| Weryfikacja własności domeny | Brak | apple-app-site-association | assetlinks.json + SHA-256 |
| Ryzyko przejęcia linku | Wysokie | Brak | Brak (przy autoVerify) |
| Fallback do przeglądarki | Nie | Tak | Tak |
| Minimalna wersja OS | iOS 2, Android 1 | iOS 9 | Android 6 |
| Konfiguracja po stronie serwera | Brak | Wymagana | Wymagana |
| Wsparcie w .NET MAUI 10 | Pełne | Pełne (Associated Domains) | Pełne (intent-filter) |
| Rekomendowane w 2026? | Tylko jako fallback | Tak (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.
Konfiguracja iOS Universal Links krok po kroku
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ą.
Konfiguracja Android App Links krok po kroku
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 dliOS, 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.sklepDla 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.
5. Deep link zabija stos nawigacji
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
Czy Universal Links wymagają certyfikatu SSL?
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.
Jaka jest różnica między deep linkiem a Universal Link?
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/.
Dlaczego mój App Link nie otwiera aplikacji tylko przeglądarkę?
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.
Jak przetestować deep link zanim aplikacja trafi do sklepu?
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.




