Web Share Target API in a Next.js PWA

Mohamed Almadih

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.ts2import { MetadataRoute } from "next";34export default function manifest(): MetadataRoute.Manifest {5return {6// ... other manifest properties7share_target: {8action: "/share-target",9method: "POST",10enctype: "multipart/form-data",11params: {12files: [13{14name: "file",15accept: ["image/*"],16},17],18},19},20};21}
Here, we define:
action: The URL that will receive the share request.method: We usePOSTbecause we are sharing files.enctype: Must bemultipart/form-datafor file uploads.params: Specifies that we expect a file namedfilewith 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.ts2self.addEventListener("fetch", (event: FetchEvent) => {3const url = new URL(event.request.url);45if (event.request.method === "POST" && url.pathname.endsWith("/share-target")) {6event.respondWith(7(async () => {8try {9const formData = await event.request.formData();10const file = formData.get("file");1112if (file instanceof File) {13const cache = await caches.open("shared-files");14// Store the file in the Web Cache API15await cache.put("/shared-image", new Response(file));16}1718// Redirect to the dashboard with a query parameter19const redirectUrl = new URL("/dashboard?shared=1", self.location.origin).href;20return Response.redirect(redirectUrl, 303);21} catch (error) {22console.error("Service Worker: Error handling share-target:", error);23const errorUrl = new URL("/dashboard?error=share_failed", self.location.origin).href;24return 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.tsx2useEffect(() => {3if (searchParams.get("shared") === "1") {4const handleSharedFile = async () => {5try {6const cache = await caches.open("shared-files");7const response = await cache.match("/shared-image");8if (response) {9const blob = await response.blob();10const file = new File([blob], "shared-receipt.jpg", {11type: blob.type || "image/jpeg",12});1314// Set the file in our form state and open the dialog15setFormData((prev) => ({ ...prev, file }));16setIsOpen(true);1718// Clean up: remove the file from cache and shared flag from URL19await cache.delete("/shared-image");20const newParams = new URLSearchParams(searchParams.toString());21newParams.delete("shared");22router.replace(`/dashboard?${newParams.toString()}`);23}24} catch (error) {25console.error("Error retrieving shared file:", error);26}27};28handleSharedFile();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.ts2import { redirect } from "next/navigation";34export async function POST() {5// If the SW fails to intercept, we fall back to a standard redirect6return redirect("/dashboard?error=share_fallback");7}89export async function GET() {10return 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:
- It handles large file transfers via
POST. - It uses the Service Worker to avoid actual server uploads until the user is ready.
- It provides a clean UI flow where the shared item automatically opens in the relevant form.