loading
Note:
Manifest first, code second Everything on this page starts in manifest.webmanifest. The manifest is what makes the browser offer an install, register the app for a file type or a scheme, and put it in the system share sheet; the C# below is only what happens after the OS has routed something to your app.

Web Share Target - the receiving half

share_target

Butil's Navigator.Share sends; Web Share Target receives. There is no JavaScript API for it at all: an installed app declares a share_target in its manifest and the OS then lists it in the system share sheet. A GET target arrives as an ordinary navigation with query parameters; a POST target arrives as a multipart form, which needs a service worker to intercept - a Blazor client cannot read a POST body the browser posted to it.

JSON
{
  "share_target": {
    "action": "/share",
    "method": "GET",
    "params": { "title": "title", "text": "text", "url": "url" }
  }
}

Reading a GET share target

Location.GetSearch

With method GET the share is just a navigation to your action URL, so the shared data is in the query string and any Blazor page can read it. This is the one share-target shape worth reaching for first.

@page "/share"
@inject Bit.Butil.Location location

<h1>@_sharedTitle</h1>
<p>@_sharedUrl</p>

@code {
    private string? _sharedTitle;
    private string? _sharedUrl;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender is false) return;

        var query = await location.GetSearch();
        var parsed = System.Web.HttpUtility.ParseQueryString(query);
        _sharedTitle = parsed["title"];
        _sharedUrl = parsed["url"];

        StateHasChanged();
    }
}

Receiving shared files

share_target + LaunchQueue

A share that carries files must be a POST target with enctype multipart/form-data, and the files reach the app through a service worker that intercepts the request. Where the browser routes the share as a launch instead, LaunchQueue.SetConsumer is what receives it - the same handler that serves file_handlers.

{
  "share_target": {
    "action": "/share",
    "method": "POST",
    "enctype": "multipart/form-data",
    "params": {
      "files": [{ "name": "media", "accept": ["image/*"] }]
    }
  }
}

Custom URL schemes

Navigator.RegisterProtocolHandler

Offers this site as the handler for a URL scheme, so a web+butil: link brings the user here. The scheme must be safelisted (mailto, tel, sms, webcal…) or prefixed with web+, and the url must be same-origin and contain a single %s placeholder. Nothing observable happens on success - the browser asks the user, in its own time.

C#
@inject Bit.Butil.Navigator navigator

var registered = await navigator.RegisterProtocolHandler("web+butil", "/open?link=%s");
Live sample
protocol handler output
Results will appear here when you interact with the samples.

Is the native app already installed?

Navigator.GetInstalledRelatedApps

Reports which of the apps your manifest claims in related_applications are actually installed - the check behind 'you already have our app, open it there'. Deliberately not an install enumerator: an app the manifest doesn't claim is never reported, and the relationship has to be proven from the other side too.

C#
var installed = await navigator.GetInstalledRelatedApps();

if (installed.Any(app => app.Platform == "play"))
{
    // don't offer the web install - they already have the Android app
}
Live sample
related apps output
Results will appear here when you interact with the samples.

Where the rest lives

The three parts of this story that are real APIs each have their own page: InstallPrompt for the install button, LaunchQueue for files and launch URLs arriving from the OS, and WindowControlsOverlay for a desktop app that draws its own title bar.

An unhandled error has occurred. Reload 🗙