diff --git a/app/api/og/collection/[id]/route.tsx b/app/api/og/collection/[id]/route.tsx
index 66160d14..59c944bc 100644
--- a/app/api/og/collection/[id]/route.tsx
+++ b/app/api/og/collection/[id]/route.tsx
@@ -76,6 +76,10 @@ export async function GET(
return new ImageResponse(
(
+ // Root carries NO padding — absolutely-positioned children measure
+ // from the padding box, so any root padding would inset the full-bleed
+ // cover image and leave the bg colour showing as a strip. Padding lives
+ // on the inner content layer instead.
{hasImage ? (
@@ -119,72 +122,80 @@ export async function GET(
>
) : null}
- {/* Eyebrow */}
+ {/* Content layer — holds the padding so the bg image stays full-bleed. */}
- Collection
-
-
- {/* Title — the centrepiece */}
-
48 ? 60 : 76,
- fontWeight: 700,
- color: textPrimary,
- lineHeight: 1.15,
- marginTop: 24,
- maxHeight: 320,
- overflow: "hidden",
- zIndex: 1,
- }}
- >
- {title}
-
-
- {description ? (
+ {/* Eyebrow */}
- {description}
+ Collection
- ) : null}
-
-
- {/* Footer: count + branding */}
-
-
- {count}
+ {/* Title — the centrepiece */}
+
48 ? 60 : 76,
+ fontWeight: 700,
+ color: textPrimary,
+ lineHeight: 1.15,
+ marginTop: 24,
+ maxHeight: 320,
+ overflow: "hidden",
+ }}
+ >
+ {title}
-
- prabhupadasays.org
+
+ {description ? (
+
+ {description}
+
+ ) : null}
+
+
+
+ {/* Footer: count + branding */}
+
+
+ {count}
+
+
+ prabhupadasays.org
+
diff --git a/app/api/og/post/[id]/route.tsx b/app/api/og/post/[id]/route.tsx
index 6fbae81e..d1d0d82d 100644
--- a/app/api/og/post/[id]/route.tsx
+++ b/app/api/og/post/[id]/route.tsx
@@ -102,7 +102,6 @@ export async function GET(
backgroundColor: hasImage ? "#1a1410" : "#faf5f0",
fontFamily: "Noto Sans, sans-serif",
position: "relative",
- padding: 80,
}}
>
{/* Background image + dimming overlay */}
@@ -138,6 +137,19 @@ export async function GET(
>
) : null}
+ {/* Content layer — holds the padding so the bg image stays
+ full-bleed (absolute children measure from the padding box, so
+ padding on the root would inset the image and show a strip). */}
+
{/* Caption — the centrepiece */}
+
),
{
diff --git a/components/social/post/post-card-menu.tsx b/components/social/post/post-card-menu.tsx
index e488b04a..34e7b39b 100644
--- a/components/social/post/post-card-menu.tsx
+++ b/components/social/post/post-card-menu.tsx
@@ -152,29 +152,60 @@ export function PostCardMenu({
// Copy the post's caption + source citation + a (short) link to the post —
// the same payload the share sheet / footer produce. Available to every
// viewer.
- async function copyCaption() {
- setOpen(false);
- try {
- const origin = window.location.origin;
- let url = "";
- if (authorHandle) {
- const longPath = `/u/${encodeURIComponent(authorHandle)}/posts#post-${post.id}`;
- const code = await createShortLink(longPath);
- url = code
- ? new URL(`/share/${code}`, origin).toString()
- : new URL(longPath, origin).toString();
- }
- const ref = structuredReferenceForPost(post.source_url, post.source_title);
- const lines: string[] = [];
- if (post.caption) lines.push(`“${post.caption}”`, "");
- lines.push("Source:", ref.title);
- if (ref.detail) lines.push(ref.detail);
- if (url) lines.push(url);
- await navigator.clipboard.writeText(lines.join("\n"));
- toast.success("Caption copied");
- } catch {
- toast.error("Could not copy");
+ // Build the copy payload — caption + citation + a short link to the post.
+ // The short link is a network round-trip, so we must NOT await it before
+ // touching the clipboard or we lose the click's transient user activation
+ // (the write then fails on the first click and only lands on the second,
+ // once the link is cached). See `useCopyFeedback` for the same fix.
+ async function buildCaptionText(): Promise
{
+ const origin = window.location.origin;
+ let url = "";
+ if (authorHandle) {
+ const longPath = `/u/${encodeURIComponent(authorHandle)}/posts#post-${post.id}`;
+ const code = await createShortLink(longPath);
+ url = code
+ ? new URL(`/share/${code}`, origin).toString()
+ : new URL(longPath, origin).toString();
}
+ const ref = structuredReferenceForPost(post.source_url, post.source_title);
+ const lines: string[] = [];
+ if (post.caption) lines.push(`“${post.caption}”`, "");
+ lines.push("Source:", ref.title);
+ if (ref.detail) lines.push(ref.detail);
+ if (url) lines.push(url);
+ return lines.join("\n");
+ }
+
+ function copyCaption() {
+ setOpen(false);
+ // Hand the clipboard a *Promise* of the text via ClipboardItem so the
+ // browser keeps the gesture alive while the short link resolves — one
+ // click copies cleanly.
+ if (typeof ClipboardItem !== "undefined" && navigator.clipboard?.write) {
+ try {
+ const item = new ClipboardItem({
+ "text/plain": buildCaptionText().then(
+ (s) => new Blob([s], { type: "text/plain" }),
+ ),
+ });
+ navigator.clipboard.write([item]).then(
+ () => toast.success("Caption copied"),
+ () => toast.error("Could not copy"),
+ );
+ return;
+ } catch {
+ // ClipboardItem-with-promise unsupported → fall through to the
+ // await + writeText path below.
+ }
+ }
+ void buildCaptionText().then(async (text) => {
+ try {
+ await navigator.clipboard.writeText(text);
+ toast.success("Caption copied");
+ } catch {
+ toast.error("Could not copy");
+ }
+ });
}
// Build the items list AFTER all hooks have run so a conditional