feat: share post to other apps

This commit is contained in:
gosangam
2026-05-31 08:44:29 +05:30
parent 88f1b08675
commit 5506afa758
5 changed files with 276 additions and 36 deletions
+78
View File
@@ -0,0 +1,78 @@
/**
* Same-origin proxy for a post's first attached image.
*
* Why this exists:
*
* navigator.share({ files }) needs a Blob built from a successful
* cross-origin fetch. The R2 bucket sits behind a custom domain
* (`s3.prabhupadasays.com`), so even with a correct CORS policy on
* the bucket, the browser will reuse pre-CORS cached responses from
* when the image was first loaded by an <img> tag without a `Vary:
* Origin` header. The cached copy lacks `Access-Control-Allow-Origin`,
* so subsequent CORS fetches from the share flow blow up with a CORS
* error even though R2 itself is now configured correctly.
*
* Routing the fetch through this same-origin endpoint sidesteps the
* whole CORS layer. The browser's request to /api/share-image/<id>
* is same-origin and never carries an Origin header to R2; Next.js
* fetches the image server-side (no CORS at all) and pipes the
* bytes back to the browser.
*
* Security: we fetch by post id (not by raw URL) so the route can't
* be coerced into SSRF — only images attached to a real post are
* reachable, and the post lookup goes through the BFF.
*/
import { bff } from "@/lib/bff";
export const runtime = "nodejs";
type PostShape = {
id: string;
images: { url: string }[];
};
export async function GET(
_request: Request,
context: { params: Promise<{ id: string }> },
) {
try {
const { id } = await context.params;
const post = await bff<PostShape>(`/posts/${encodeURIComponent(id)}`, {
revalidate: 60,
});
if (!post || !post.images?.[0]?.url) {
return new Response("Image not found", { status: 404 });
}
const imageUrl = post.images[0].url;
// Server-side fetch — no CORS at all from this side of the wire.
const upstream = await fetch(imageUrl);
if (!upstream.ok || !upstream.body) {
return new Response("Upstream fetch failed", {
status: upstream.status || 502,
});
}
const contentType =
upstream.headers.get("content-type") || "image/jpeg";
const contentLength = upstream.headers.get("content-length");
return new Response(upstream.body, {
status: 200,
headers: {
"Content-Type": contentType,
// Allow shared CDN caching for a day — image content for a
// given post id is immutable in practice (replacing the image
// mints a new key), so a long TTL is safe. Browser cache
// shorter so updates are picked up faster on the share flow.
"Cache-Control":
"public, max-age=3600, s-maxage=86400, stale-while-revalidate=86400",
...(contentLength ? { "Content-Length": contentLength } : {}),
},
});
} catch (e) {
console.error("share-image proxy failed:", e);
return new Response("Proxy error", { status: 500 });
}
}