clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

feat/184-task-notice-migration
lofi / docs / recipes / web-share.md
web-share.md 5.1K rendered
1# Inbound and outbound Web Share23Use this recipe when a user should explicitly share a small text/link payload through the OS share4sheet or open an installed lofi app with shared text as an uncommitted draft.56The [outbound Web Share API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API) and7inbound8[`share_target` manifest member](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/share_target)9have limited browser support. Both require a secure deployed context; the inbound target appears10only after installation. Unsupported browsers must retain an ordinary copy/link or form flow.1112## Outbound share1314Import the isolated recipe entrypoint. Call it directly from the click handler so the browser still15has transient user activation:1617```tsx18import { shareOrFallback } from "@nzip/lofi/recipes/web-share";1920<button21 type="button"22 onClick={() => {23 void shareOrFallback(24 { title: task.text, text: task.text, url: new URL(`#task-${task.id}`, location.href).href },25 {26 fallback: async (data) => {27 await navigator.clipboard.writeText(data.url ?? data.text ?? "");28 },29 },30 ).then((outcome) => {31 // shared: native sheet completed32 // cancelled: user closed the native sheet; do not show an error33 // fallback: the normal copy action ran34 // failed: offer an explicit retry or copy button35 setShareOutcome(outcome);36 });37 }}38>39 Share task40</button>;41```4243The helper calls `navigator.canShare()` when present. It falls back only when native sharing is44missing or rejects the payload before opening. Cancellation is a normal result; a runtime failure45does not silently copy data after the native sheet may already have opened.4647If Clipboard is also unavailable, make the fallback reveal a normal selected text field or link48instead of claiming that copying succeeded.4950## Inbound text/link target5152Add an explicit opt-in member to `public/manifest.webmanifest`:5354```json55{56 "share_target": {57 "action": "./share/",58 "method": "GET",59 "enctype": "application/x-www-form-urlencoded",60 "params": {61 "title": "title",62 "text": "text",63 "url": "url"64 }65 }66}67```6869This recipe intentionally accepts text and HTTP(S) links with GET. POST/file shares require worker70request handling, temporary storage, file validation, and a redirect; lofi does not pretend that the71baseline worker provides those behaviors.7273Create `src/pages/share.astro` as a prerendered route:7475```astro76---77import ShareReceiver from "../islands/ShareReceiver.tsx";78import Shell from "../layouts/Shell.astro";79---8081<Shell title="Review shared content">82 <ShareReceiver client:load />83</Shell>84```8586Parse the current query only in the browser. The parser ignores unknown parameters, rejects87duplicates and over-limit values, and accepts only absolute HTTP(S) URLs:8889```tsx90import { useEffect, useState } from "preact/hooks";91import { parseTextShareTarget, type TextShareTargetResult } from "@nzip/lofi/recipes/web-share";9293export default function ShareReceiver() {94 const [result, setResult] = useState<TextShareTargetResult>();9596 useEffect(() => {97 setResult(parseTextShareTarget(location.search));98 }, []);99100 if (!result) return <p role="status">Opening shared draft…</p>;101 if (!result.ok) return <p role="alert">This shared content is not valid.</p>;102103 return (104 <form105 onSubmit={(event) => {106 event.preventDefault();107 // Persist result.draft only here, after the user confirms.108 }}109 >110 <h1>Review shared content</h1>111 {result.draft.title && <p>{result.draft.title}</p>}112 {result.draft.text && <p>{result.draft.text}</p>}113 {result.draft.url && <a href={result.draft.url}>{result.draft.url}</a>}114 <button type="submit">Add to app</button>115 </form>116 );117}118```119120Change the default parameter names or limits only when the manifest and parser options change121together. Never render a received URL as HTML, use it as an arbitrary redirect, or persist the draft122on page load.123124## Build, install, and remove125126`deno task doctor` validates the member shape and same-scope action. `deno task build` also requires127the action to map to a prerendered route, which puts that HTML and its assets in the offline shell.128129Manifest capability changes do not propagate uniformly across installed platforms. After adding or130removing the target, verify a fresh install and an existing installation; some platforms require131reinstallation before the OS share sheet changes. Removing the member and receiver route restores132the ordinary web app without affecting stored lofi data.133134## Verification checklist1351361. Open `/share/` directly with no query and show a fixed invalid-draft message.1372. Open it with valid title, text, and HTTP(S) URL values and require confirmation before saving.1383. Reject duplicate, malformed, non-HTTP(S), and over-limit values without echoing them in errors.1394. Install the app, share into it from another app, and verify an offline cold start of the route.1405. Exercise outbound shared, cancelled, unsupported/fallback, and runtime-failed outcomes.1416. Remove `share_target`, rebuild, and confirm the rest of the PWA remains installable and offline.