feat: share post to other apps

This commit is contained in:
gosangam
2026-05-31 08:04:46 +05:30
parent 03b8f85e06
commit 336f478f8b
13 changed files with 1290 additions and 82 deletions
+278
View File
@@ -0,0 +1,278 @@
/**
* OG card for a single post.
*
* Renders a 1200×630 image with:
*
* • The post caption (the quote) as the visual centrepiece, in
* serif-toned weight so it reads as a passage rather than UI chrome.
* • The source reference below — citation for the underlying scripture.
* • Author byline (avatar + name + @handle) bottom-left.
* • The post's first attached image as a dimmed background when
* available — same content the author sees on the card itself.
*
* Falls back to a flat cream background when the post has no images.
* The light/dark text treatment swaps based on which surface is showing.
*/
import { ImageResponse } from "next/og";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { bff } from "@/lib/bff";
export const runtime = "nodejs";
type PostImage = { id: string; url: string; position: number };
type Post = {
id: string;
user_id: string;
caption: string;
source_url: string;
source_title: string;
images: PostImage[];
author_name?: string;
author_handle?: string;
author_avatar_url?: string | null;
};
let notoRegularData: Buffer | null = null;
let notoBoldData: Buffer | null = null;
async function loadFonts() {
const fontsDir = join(process.cwd(), "public/fonts/Noto_Sans/static");
if (!notoRegularData) {
notoRegularData = await readFile(join(fontsDir, "NotoSans-Regular.ttf"));
}
if (!notoBoldData) {
notoBoldData = await readFile(join(fontsDir, "NotoSans-Bold.ttf"));
}
return { regular: notoRegularData, bold: notoBoldData };
}
function initialsFor(name: string): string {
return (
name
.split(/\s+/)
.filter(Boolean)
.map((p) => p[0])
.slice(0, 2)
.join("")
.toUpperCase() || "?"
);
}
export async function GET(
_request: Request,
context: { params: Promise<{ id: string }> },
) {
try {
const { id } = await context.params;
const post = await bff<Post>(`/posts/${encodeURIComponent(id)}`, {
revalidate: 60,
}).catch(() => null);
if (!post) {
return new Response("Post not found", { status: 404 });
}
const fonts = await loadFonts();
const caption = post.caption || "";
const displayCaption =
caption.length > 280 ? caption.slice(0, 277) + "…" : caption;
const reference = post.source_title || "Source";
const authorName = post.author_name || "Unknown";
const authorHandle = post.author_handle ? `@${post.author_handle}` : "";
const bgImageUrl = post.images?.[0]?.url ?? null;
const hasImage = !!bgImageUrl;
// Colour palette flips for image-backed cards so the caption stays
// legible over the dimmed photo.
const textPrimary = hasImage ? "#ffffff" : "#2d2418";
const textSecondary = hasImage ? "rgba(255,255,255,0.85)" : "#5a4a3a";
const accent = hasImage ? "#ffd9a8" : "#c47a5a";
return new ImageResponse(
(
<div
style={{
width: 1200,
height: 630,
display: "flex",
flexDirection: "column",
backgroundColor: hasImage ? "#1a1410" : "#faf5f0",
fontFamily: "Noto Sans, sans-serif",
position: "relative",
padding: 80,
}}
>
{/* Background image + dimming overlay */}
{hasImage ? (
<>
<img
src={bgImageUrl}
width={1200}
height={630}
style={{
position: "absolute",
top: 0,
left: 0,
width: 1200,
height: 630,
objectFit: "cover",
filter: "brightness(0.45)",
}}
/>
{/* Bottom-up gradient anchors the caption against the image */}
<div
style={{
position: "absolute",
top: 0,
left: 0,
width: 1200,
height: 630,
background:
"linear-gradient(to top, rgba(0,0,0,0.75), rgba(0,0,0,0.1))",
}}
/>
</>
) : null}
{/* Caption — the centrepiece */}
<div
style={{
display: "flex",
fontSize: displayCaption.length > 160 ? 36 : 44,
fontWeight: 700,
color: textPrimary,
lineHeight: 1.35,
marginTop: 20,
maxHeight: 380,
overflow: "hidden",
zIndex: 1,
}}
>
“{displayCaption}”
</div>
{/* Reference (source citation) */}
<div
style={{
display: "flex",
fontSize: 22,
fontWeight: 700,
color: textSecondary,
letterSpacing: 4,
textTransform: "uppercase",
marginTop: 28,
zIndex: 1,
}}
>
{reference}
</div>
{/* Spacer pushes the byline + branding to the bottom */}
<div style={{ flex: 1, display: "flex" }} />
{/* Bottom row: author + branding */}
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
zIndex: 1,
}}
>
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 16,
}}
>
{post.author_avatar_url ? (
<img
src={post.author_avatar_url}
width={64}
height={64}
style={{
width: 64,
height: 64,
borderRadius: 32,
objectFit: "cover",
border: `3px solid ${textPrimary}`,
}}
/>
) : (
<div
style={{
width: 64,
height: 64,
borderRadius: 32,
backgroundColor: accent,
color: hasImage ? "#1a1410" : "#faf5f0",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 28,
fontWeight: 700,
}}
>
{initialsFor(authorName)}
</div>
)}
<div
style={{ display: "flex", flexDirection: "column", gap: 2 }}
>
<div
style={{
fontSize: 22,
fontWeight: 700,
color: textPrimary,
display: "flex",
}}
>
{authorName}
</div>
{authorHandle ? (
<div
style={{
fontSize: 18,
color: textSecondary,
display: "flex",
}}
>
{authorHandle}
</div>
) : null}
</div>
</div>
<div style={{ fontSize: 18, color: accent, fontWeight: 700 }}>
prabhupadasays.org
</div>
</div>
</div>
),
{
width: 1200,
height: 630,
fonts: [
{
name: "Noto Sans",
data: fonts.regular,
style: "normal" as const,
weight: 400,
},
{
name: "Noto Sans",
data: fonts.bold,
style: "normal" as const,
weight: 700,
},
],
},
);
} catch (e) {
console.error("Post OG image generation failed:", e);
return new Response("Failed to generate OG image", { status: 500 });
}
}