Web Share Target API in a Next.js PWA

Mohamed Almadih

Mohamed Almadih

1/31/2026
4 min read
Web Share Target API in a Next.js PWA

Implementing the Web Share Target API in a Next.js PWA

In modern web development, Progressive Web Apps (PWAs) are closing the gap between web and native applications. One of the most powerful features to achieve this is the Web Share Target API. This API allows your PWA to appear in the native share sheet of the operating system, making it possible for users to "share" content—like images or text—directly from other apps into yours.

In this post, I'll walk through how we implemented this feature in BayanPlus to allow users to share bank receipts directly into their dashboard.

1. Registering the Share Target in the Manifest

The first step is to tell the browser that your app is capable of handling shared content. This is done in the manifest.json (or manifest.ts in Next.js).

1
// src/app/manifest.ts
2
import { MetadataRoute } from "next";
3
4
export default function manifest(): MetadataRoute.Manifest {
5
return {
6
// ... other manifest properties
7
share_target: {
8
action: "/share-target",
9
method: "POST",
10
enctype: "multipart/form-data",
11
params: {
12
files: [
13
{
14
name: "file",
15
accept: ["image/*"],
16
},
17
],
18
},
19
},
20
};
21
}

Here, we define:

  • action: The URL that will receive the share request.
  • method: We use POST because we are sharing files.
  • enctype: Must be multipart/form-data for file uploads.
  • params: Specifies that we expect a file named file with an image mime-type.

2. Intercepting the Request in the Service Worker

When a user shares a file, the browser sends a POST request to /share-target. However, since this is a PWA, we want to handle this request even if the user is offline or if we want to provide a smoother transition. We use the Service Worker to intercept this POST request.

1
// worker/index.ts
2
self.addEventListener("fetch", (event: FetchEvent) => {
3
const url = new URL(event.request.url);
4
5
if (event.request.method === "POST" && url.pathname.endsWith("/share-target")) {
6
event.respondWith(
7
(async () => {
8
try {
9
const formData = await event.request.formData();
10
const file = formData.get("file");
11
12
if (file instanceof File) {
13
const cache = await caches.open("shared-files");
14
// Store the file in the Web Cache API
15
await cache.put("/shared-image", new Response(file));
16
}
17
18
// Redirect to the dashboard with a query parameter
19
const redirectUrl = new URL("/dashboard?shared=1", self.location.origin).href;
20
return Response.redirect(redirectUrl, 303);
21
} catch (error) {
22
console.error("Service Worker: Error handling share-target:", error);
23
const errorUrl = new URL("/dashboard?error=share_failed", self.location.origin).href;
24
return Response.redirect(errorUrl, 303);
25
}
26
})(),
27
);
28
}
29
});

Why a 303 Redirect?

We use a 303 See Other status code. This is crucial because it tells the browser to perform a GET request to the redirection target, effectively converting the POST share into a GET navigation to our dashboard.

3. Retrieving the Shared File in the Frontend

Once the user is redirected to /dashboard?shared=1, our React components take over. We check for the shared parameter and, if present, reach into the Web Cache to retrieve the file we stored earlier.

1
// src/app/(dashboard)/dashboard/_components/upload-receipt-form.tsx
2
useEffect(() => {
3
if (searchParams.get("shared") === "1") {
4
const handleSharedFile = async () => {
5
try {
6
const cache = await caches.open("shared-files");
7
const response = await cache.match("/shared-image");
8
if (response) {
9
const blob = await response.blob();
10
const file = new File([blob], "shared-receipt.jpg", {
11
type: blob.type || "image/jpeg",
12
});
13
14
// Set the file in our form state and open the dialog
15
setFormData((prev) => ({ ...prev, file }));
16
setIsOpen(true);
17
18
// Clean up: remove the file from cache and shared flag from URL
19
await cache.delete("/shared-image");
20
const newParams = new URLSearchParams(searchParams.toString());
21
newParams.delete("shared");
22
router.replace(`/dashboard?${newParams.toString()}`);
23
}
24
} catch (error) {
25
console.error("Error retrieving shared file:", error);
26
}
27
};
28
handleSharedFile();
29
}
30
}, [searchParams, router]);

4. The Fallback Route

While the Service Worker handles the interception, it's good practice to have a server-side route handler at /share-target in case the Service Worker isn't active or fails.

1
// src/app/(dashboard)/share-target/route.ts
2
import { redirect } from "next/navigation";
3
4
export async function POST() {
5
// If the SW fails to intercept, we fall back to a standard redirect
6
return redirect("/dashboard?error=share_fallback");
7
}
8
9
export async function GET() {
10
return redirect("/dashboard");
11
}

Conclusion

Implementing the Share Target API transforms your PWA from a simple website into a deeply integrated tool on the user's device. By combining the Manifest, Service Worker redirection, and the Web Cache API, we created a seamless experience where sharing a receipt is as easy as sharing a photo with a friend.

This pattern is robust because:

  1. It handles large file transfers via POST.
  2. It uses the Service Worker to avoid actual server uploads until the user is ready.
  3. It provides a clean UI flow where the shared item automatically opens in the relevant form.

Related Posts

Server-Side Data Fetching + URL Search Params with nuqs in Next.js

When building dashboards in Next.js, you often need filters such as search bars, dropdowns, and pagination controls...