
Maui Deep Linking
- 29 installs
- 163 repo stars
- Updated July 6, 2026
- davidortinau/maui-skills
Implements deep linking in .NET MAUI apps via Android App Links with Digital Asset Links and iOS Universal Links.
About
Guides implementing deep linking in .NET MAUI apps covering Android App Links with intent filters, Digital Asset Links and AutoVerify, plus iOS Universal Links. A developer uses it when routing external URLs into specific MAUI app screens.
- Android App Links with intent filters and Digital Asset Links
- iOS Universal Links with AutoVerify
Maui Deep Linking by the numbers
- 29 all-time installs (skills.sh)
- Ranked #672 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/davidortinau/maui-skills --skill maui-deep-linkingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 29 |
|---|---|
| repo stars | ★ 163 |
| Last updated | July 6, 2026 |
| Repository | davidortinau/maui-skills ↗ |
What it does
Implements deep linking in .NET MAUI apps via Android App Links with Digital Asset Links and iOS Universal Links.
Files
.NET MAUI Deep Linking
Platform Gotchas
Android
- `AutoVerify = true` is required on the
IntentFilterfor App Links (not
just deep links). Without it, Android shows a disambiguation dialog instead of opening your app directly.
- Handle intent in both `OnCreate` and `OnNewIntent`.
OnCreate fires for cold starts; OnNewIntent fires when the app is already running. Missing either means links silently fail in one scenario.
// ❌ Only handles cold-start links
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
HandleDeepLink(Intent);
}
// ✅ Handles both cold-start and warm-start links
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
HandleDeepLink(Intent);
}
protected override void OnNewIntent(Intent? intent)
{
base.OnNewIntent(intent);
HandleDeepLink(intent);
}- SHA-256 fingerprint must match the signing key used for the build you're
testing. Debug and release builds use different keys — update assetlinks.json accordingly or verification silently fails.
- Test verification status with:
adb shell pm get-app-links com.example.myappLook for verified status, not just ask.
iOS
- ⚠️ Universal Links do NOT work in the Simulator. You must test on a
physical device.
- AASA changes take up to 24 hours to propagate through Apple's CDN
(iOS 14+). During development, use the ?mode=developer query param or Apple's CDN diagnostics: swcutil dl -d example.com.
- Handle all three entry points:
FinishedLaunching,ContinueUserActivity,
and SceneWillConnect. Missing any one causes links to fail for specific app states (cold start, background resume, or scene-based launch).
- `applinks:` prefix is required in the Associated Domains entitlement.
Writing just example.com instead of applinks:example.com silently fails.
---
Common Mistakes
Forgetting MainThread.BeginInvokeOnMainThread
Deep link callbacks can fire on background threads. Shell navigation must run on the main thread.
// ❌ May crash — GoToAsync called off the main thread
static void HandleUniversalLink(string? url)
{
if (string.IsNullOrEmpty(url)) return;
Shell.Current.GoToAsync(MapToRoute(url));
}
// ✅ Dispatch to main thread
static void HandleUniversalLink(string? url)
{
if (string.IsNullOrEmpty(url)) return;
MainThread.BeginInvokeOnMainThread(async () =>
await Shell.Current.GoToAsync(MapToRoute(url)));
}Route not registered before navigation
Register Shell routes in AppShell constructor before any deep link can fire. If the route doesn't exist, GoToAsync throws silently or navigates to root.
Custom URI schemes vs. App Links / Universal Links
| Approach | Verified | Fallback to browser | Recommended |
|---|---|---|---|
Custom URI scheme (myapp://) | No | No | Only for app-to-app communication |
Android App Links (https://) | Yes | Yes | ✅ Production web links |
iOS Universal Links (https://) | Yes | Yes | ✅ Production web links |
⚠️ Custom URI schemes are not verified — any app can register the same
scheme. Use https:// App Links / Universal Links for user-facing URLs.---
Debugging Checklist
- [ ] Android:
IntentFilterhasAutoVerify = trueonMainActivity - [ ] Android:
assetlinks.jsonat/.well-known/with correct SHA-256 for current signing key - [ ] Android: Intent handled in both
OnCreateandOnNewIntent - [ ] Android: Verified with
adb shell pm get-app-links - [ ] iOS:
applinks:example.comin Associated Domains entitlement (not justexample.com) - [ ] iOS: AASA file at
/.well-known/apple-app-site-associationwith correct Team ID - [ ] iOS: All three lifecycle entry points handled
- [ ] iOS: Tested on physical device (not simulator)
- [ ] Shell routes registered before deep link callbacks fire
- [ ] Navigation dispatched to main thread
Deep Linking API Reference
Android App Links
IntentFilter on MainActivity
[IntentFilter(
new[] { Android.Content.Intent.ActionView },
Categories = new[] {
Android.Content.Intent.CategoryDefault,
Android.Content.Intent.CategoryBrowsable
},
DataScheme = "https",
DataHost = "example.com",
DataPathPrefix = "/products",
AutoVerify = true)]
public class MainActivity : MauiAppCompatActivity { }Stack multiple IntentFilter attributes for different paths.
Digital Asset Links (domain verification)
Host /.well-known/assetlinks.json on your domain over HTTPS:
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.myapp",
"sha256_cert_fingerprints": ["AA:BB:CC:..."]
}
}]Get SHA-256: keytool -list -v -keystore my-release-key.keystore -alias alias_name
Handle incoming intents
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
HandleDeepLink(Intent);
}
protected override void OnNewIntent(Intent? intent)
{
base.OnNewIntent(intent);
HandleDeepLink(intent);
}
void HandleDeepLink(Intent? intent)
{
if (intent?.Action != Intent.ActionView || intent.Data is null) return;
Shell.Current.GoToAsync(MapToRoute(intent.Data.ToString()!));
}Test commands
adb shell am start -W -a android.intent.action.VIEW \
-d "https://example.com/products/42" com.example.myapp
adb shell pm get-app-links com.example.myapp---
iOS Universal Links
Associated Domains entitlement
In Entitlements.plist:
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:example.com</string>
</array>Apple App Site Association file
Host at /.well-known/apple-app-site-association (Content-Type: application/json):
{
"applinks": {
"details": [{
"appIDs": ["TEAMID.com.example.myapp"],
"components": [{ "/": "/products/*" }]
}]
}
}Handle Universal Links in MAUI
builder.ConfigureLifecycleEvents(events =>
{
#if IOS || MACCATALYST
events.AddiOS(ios =>
{
ios.FinishedLaunching((app, options) =>
{
var activity = options?[UIKit.UIApplication.LaunchOptionsUniversalLinkKey]
as Foundation.NSUserActivity;
HandleUniversalLink(activity?.WebPageUrl?.ToString());
return true;
});
ios.ContinueUserActivity((app, activity, handler) =>
{
if (activity.ActivityType == Foundation.NSUserActivityType.BrowsingWeb)
HandleUniversalLink(activity.WebPageUrl?.ToString());
return true;
});
ios.SceneWillConnect((scene, session, options) =>
{
var activity = options.UserActivities?
.ToArray<Foundation.NSUserActivity>()
.FirstOrDefault(a =>
a.ActivityType == Foundation.NSUserActivityType.BrowsingWeb);
HandleUniversalLink(activity?.WebPageUrl?.ToString());
});
});
#endif
});
static void HandleUniversalLink(string? url)
{
if (string.IsNullOrEmpty(url)) return;
MainThread.BeginInvokeOnMainThread(async () =>
await Shell.Current.GoToAsync(MapToRoute(url)));
}---
Shell Navigation Integration
// Register in AppShell constructor
Routing.RegisterRoute("products/detail", typeof(ProductDetailPage));
static string MapToRoute(string uri)
{
var segments = new Uri(uri).AbsolutePath.Trim('/').Split('/');
return segments switch
{
["products", var id] => $"products/detail?id={id}",
["settings"] => "settings",
_ => "//"
};
}