# Receive shared content (share target)

> Register an installed PWA as a share target: declare share_target in the manifest, handle the POST in the service worker, and read the shared files in the app.

import CompatTable from '@components/CompatTable.astro';

At the end of this guide your installed PWA appears in the operating system's share sheet,
and a photo shared from another app lands in your page as a `File` you can preview and
import. `share_target` in the Web App Manifest declares the receiving URL; the browser then
issues a `GET` or `POST` to it exactly as a form submission would, and the rest is request
handling you already know.

You need an installable PWA with a service worker (see
[Make it installable](/guides/installable/)); the share sheet lists a web app only after
the user has installed it, on every platform. One `share_target` is allowed per manifest,
so route different kinds of shares on the landing page rather than declaring several.

## 1. Declare share_target in the manifest

`action` (the receiving URL, inside the manifest `scope`) and `params` (the mapping from
share fields to request parameter names) are required. `method` defaults to `GET` and
`enctype` to `application/x-www-form-urlencoded`. Receiving files requires `POST` with
`multipart/form-data` and a `files` array whose entries name the field and the accepted
MIME types or extensions; list both forms, because operating systems differ in which
they match.

```json
{
  "name": "Scrapbook",
  "start_url": "/",
  "display": "standalone",
  "share_target": {
    "action": "/share",
    "method": "POST",
    "enctype": "multipart/form-data",
    "params": {
      "title": "title",
      "text": "text",
      "url": "url",
      "files": [{ "name": "media", "accept": ["image/*", ".png", ".jpg", "video/*"] }]
    }
  }
}
```

A text-only target can keep the `GET` default: the browser opens `action` with
`?title=…&text=…&url=…` and the page reads `new URL(location).searchParams`. `GET` is
simpler to debug but leaks the shared text into history and server logs; `POST` keeps it
in the request body and is the only way to receive files.

## 2. Handle the POST in the service worker

A page cannot read a `POST` body, so the service worker intercepts the request in `fetch`,
reads `formData()`, stores the files, and answers with a `303 See Other` redirect to a page
that displays them. The redirect matters: it stops a refresh from re-submitting the share.
The redirect target carries a marker (`?shared=1`) so the page knows to look in the cache.

```js
// sw.js
self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);
  if (event.request.method !== 'POST' || url.pathname !== '/share') return;

  event.respondWith(
    (async () => {
      const formData = await event.request.formData();
      const files = formData.getAll('media');
      const cache = await caches.open('shared-content');
      await Promise.all(files.map((file, i) => cache.put(`/shared/${i}`, new Response(file))));
      const text = formData.get('text') || formData.get('url') || '';
      return Response.redirect(`/share/view?shared=1&text=${encodeURIComponent(text)}`, 303);
    })(),
  );
});
```

Validate what arrives as you would any form submission: other apps place content in
unexpected parameters, and the body comes from software you do not control.

:::observed
When a URL is shared into a web app on Android, the `url` parameter arrives empty because
Android's share system has no URL field; the link shows up in `text`, and sometimes in
`title`. The handler above reads `text` before `url` for that reason. Chrome's share
target documentation records the same behaviour.
:::

## 3. Read the shared files in the page

The redirected page checks the marker, reads the stored responses back as `Blob`s, shows
them for confirmation, and then deletes them so a second share does not pick up the first
share's files. Opening `/share/view` directly, without the marker, renders the normal view.

```js
function isSharedLaunch() {
  return new URLSearchParams(location.search).has('shared');
}

async function takeSharedFiles() {
  if (!('caches' in window)) return [];
  const cache = await caches.open('shared-content');
  const requests = await cache.keys();
  const files = await Promise.all(requests.map((request) => cache.match(request).then((r) => r.blob())));
  await Promise.all(requests.map((request) => cache.delete(request)));
  return files;
}

if (isSharedLaunch()) {
  takeSharedFiles().then(renderSharedFiles);
} else {
  renderDefaultView();
}
```

## 4. Install, then share into the app

Install the PWA from the browser's **Install** entry, open any photo app, choose **Share**,
and pick your app's name from the system sheet. The redirect lands on `/share/view` with
the previews rendered from the cached files. Share a URL from the browser as well to see
the Android `text` fallback in action. The browsers that offer an installed web app in the
share sheet are listed in the table:

<CompatTable feature="manifest-share-target" />

## See also

- [Manifest share_target](/reference/manifest/share-target/)
- [Web Share API: native share sheet from the browser](/reference/capabilities/web-share/)
- [manifest: share_target support](/compatibility/manifest-share-target/)
- [Receiving shared data with the Web Share Target API](https://developer.chrome.com/docs/capabilities/web-apis/web-share-target) (developer.chrome.com)
- [share_target](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/share_target) (developer.mozilla.org)

← Back to the [Guides](/guides/) overview.