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
+224
View File
@@ -0,0 +1,224 @@
/**
* OG card for a user profile (`/u/<handle>`).
*
* Renders a 1200×630 image with avatar (or initials fallback), display
* name, @handle, and a stats line (post count + join year). Designed
* to match the chapter-card OG aesthetic so a feed of links from
* different parts of the site looks coherent in WhatsApp / Twitter
* previews.
*
* Data comes from the public BFF profile endpoint — no auth required.
* Failures (missing profile, BFF unreachable) return 404 / 500 instead
* of a generic image so social scrapers know to fall back to a
* higher-level card; the share/[code]'s `ogDefault()` shape handles
* that case.
*/
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 PublicProfile = {
handle: string;
name: string;
avatar_url: string | null;
joined_at: string;
post_count: number;
};
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() || "?"
);
}
function formatJoined(iso: string): string {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return "";
return `${d.toLocaleString("en-US", { month: "short" })} ${d.getFullYear()}`;
}
export async function GET(
_request: Request,
context: { params: Promise<{ handle: string }> },
) {
try {
const { handle } = await context.params;
const profile = await bff<PublicProfile>(
`/u/${encodeURIComponent(handle)}`,
{ revalidate: 60 },
).catch(() => null);
if (!profile) {
return new Response("Profile not found", { status: 404 });
}
const fonts = await loadFonts();
const displayName =
profile.name.length > 40 ? profile.name.slice(0, 37) + "…" : profile.name;
const stats = [
`${profile.post_count} post${profile.post_count === 1 ? "" : "s"}`,
profile.joined_at ? `Joined ${formatJoined(profile.joined_at)}` : null,
]
.filter(Boolean)
.join(" · ");
return new ImageResponse(
(
<div
style={{
width: 1200,
height: 630,
display: "flex",
flexDirection: "column",
backgroundColor: "#faf5f0",
fontFamily: "Noto Sans, sans-serif",
padding: 80,
}}
>
{/* Top row: avatar + identity block */}
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 48,
}}
>
{profile.avatar_url ? (
<img
src={profile.avatar_url}
width={220}
height={220}
style={{
width: 220,
height: 220,
borderRadius: 110,
objectFit: "cover",
border: "6px solid #2d2418",
flexShrink: 0,
}}
/>
) : (
<div
style={{
width: 220,
height: 220,
borderRadius: 110,
backgroundColor: "#c47a5a",
color: "#faf5f0",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 96,
fontWeight: 700,
flexShrink: 0,
}}
>
{initialsFor(profile.name)}
</div>
)}
<div
style={{
display: "flex",
flexDirection: "column",
gap: 6,
flex: 1,
minWidth: 0,
}}
>
<div
style={{
fontSize: 60,
fontWeight: 700,
color: "#2d2418",
lineHeight: 1.05,
display: "flex",
overflow: "hidden",
}}
>
{displayName}
</div>
<div style={{ fontSize: 30, color: "#5a4a3a", display: "flex" }}>
@{profile.handle}
</div>
{stats ? (
<div
style={{
fontSize: 22,
color: "#5a4a3a",
marginTop: 12,
display: "flex",
}}
>
{stats}
</div>
) : null}
</div>
</div>
{/* Footer: branding */}
<div
style={{
marginTop: "auto",
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 8,
}}
>
<span style={{ fontSize: 22, color: "#5a4a3a" }}>
Read quotes shared on
</span>
<span style={{ fontSize: 22, color: "#c47a5a", fontWeight: 700 }}>
prabhupadasays.org
</span>
</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("Profile OG image generation failed:", e);
return new Response("Failed to generate OG image", { status: 500 });
}
}