Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions src/app/api/og/user/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { ImageResponse } from "@vercel/og";
import { NextRequest } from "next/server";

export const runtime = "edge";

export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);

const username = searchParams.get("username") ?? "developer";
const name = searchParams.get("name") ?? username;
const avatar = searchParams.get("avatar") ?? "";
const topLang = searchParams.get("topLang") ?? "JavaScript";
const streak = searchParams.get("streak") ?? "0";
const commits = searchParams.get("commits") ?? "0";

return new ImageResponse(
(
<div
style={{
width: "1200px",
height: "630px",
background: "linear-gradient(135deg, #0f172a 0%, #1e293b 60%, #0f172a 100%)",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
fontFamily: "sans-serif",
position: "relative",
overflow: "hidden",
}}
>
<div style={{ position: "absolute", inset: "0", backgroundImage: "radial-gradient(circle at 1px 1px, rgba(99,102,241,0.18) 1px, transparent 0)", backgroundSize: "44px 44px", display: "flex" }} />
<div style={{ position: "absolute", top: "-100px", left: "-100px", width: "420px", height: "420px", borderRadius: "50%", background: "rgba(99,102,241,0.18)", filter: "blur(90px)", display: "flex" }} />
<div style={{ position: "absolute", bottom: "-100px", right: "-100px", width: "420px", height: "420px", borderRadius: "50%", background: "rgba(16,185,129,0.12)", filter: "blur(90px)", display: "flex" }} />

<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "36px", background: "rgba(255,255,255,0.04)", border: "1.5px solid rgba(255,255,255,0.09)", borderRadius: "28px", padding: "52px 72px", zIndex: 10 }}>

<div style={{ display: "flex", alignItems: "center", gap: "28px" }}>
{avatar ? (
<img src={avatar} width={100} height={100} style={{ borderRadius: "50%", border: "3px solid rgba(99,102,241,0.7)", objectFit: "cover" }} />
) : (
<div style={{ width: "100px", height: "100px", borderRadius: "50%", background: "linear-gradient(135deg,#6366f1,#10b981)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: "44px", fontWeight: 700, color: "#fff" }}>
{username[0]?.toUpperCase()}
</div>
)}
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
<span style={{ fontSize: "38px", fontWeight: 700, color: "#f8fafc" }}>{name}</span>
<span style={{ fontSize: "20px", color: "#94a3b8" }}>@{username}</span>
</div>
</div>

<div style={{ width: "100%", height: "1px", background: "rgba(255,255,255,0.08)", display: "flex" }} />

<div style={{ display: "flex", gap: "56px" }}>
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "8px" }}>
<span style={{ fontSize: "16px", color: "#94a3b8" }}>🔥 Streak</span>
<span style={{ fontSize: "30px", fontWeight: 700, color: "#f97316" }}>{streak} days</span>
</div>
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "8px" }}>
<span style={{ fontSize: "16px", color: "#94a3b8" }}>📦 Commits</span>
<span style={{ fontSize: "30px", fontWeight: 700, color: "#6366f1" }}>{Number(commits).toLocaleString()}</span>
</div>
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "8px" }}>
<span style={{ fontSize: "16px", color: "#94a3b8" }}>⚡ Top Language</span>
<span style={{ fontSize: "30px", fontWeight: 700, color: "#10b981" }}>{topLang}</span>
</div>
</div>
</div>

<div style={{ position: "absolute", bottom: "26px", display: "flex", alignItems: "center", gap: "8px", fontSize: "15px", color: "#475569" }}>
<span style={{ color: "#6366f1", fontWeight: 700 }}>DevTrack</span>
<span>· devtrack.app/u/{username}</span>
</div>
</div>
),
{ width: 1200, height: 630 }
);
}
40 changes: 32 additions & 8 deletions src/app/u/[username]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ export async function generateMetadata({
params: Promise<{ username: string }>;
}): Promise<Metadata> {
const { username } = await params;
// Minimal lookup — avoids duplicating 3 GitHub API calls that the page already makes
const user = await getUserByUsername(username);
const profileUrl = getProfileUrl(username);

Expand All @@ -56,24 +55,49 @@ export async function generateMetadata({
};
}

const baseUrl =
process.env.NEXT_PUBLIC_APP_URL ||
process.env.NEXTAUTH_URL ||
"http://localhost:3000";

// Build dynamic OG image URL
const ogImageUrl = new URL(`${baseUrl}/api/og/user`);
ogImageUrl.searchParams.set("username", username);
ogImageUrl.searchParams.set("name", username);
ogImageUrl.searchParams.set("avatar", `https://avatars.githubusercontent.com/${username}`);
ogImageUrl.searchParams.set("topLang", "Code");
ogImageUrl.searchParams.set("streak", "0");
ogImageUrl.searchParams.set("commits", "0");

const title = `${username}'s DevTrack Profile`;
const description = `GitHub stats and coding activity for ${username}. View commits, streaks, and top repositories.`;

return {
title: `${username}'s DevTrack Profile`,
description: `GitHub stats and coding activity for ${username}. View commits, streaks, and top repositories.`,
title,
description,
openGraph: {
title: `${username}'s DevTrack Profile`,
description: `GitHub stats and coding activity for ${username}`,
title,
description,
url: profileUrl,
siteName: "DevTrack",
type: "profile",
images: [
{
url: ogImageUrl.toString(),
width: 1200,
height: 630,
alt: `${username}'s DevTrack profile`,
},
],
},
twitter: {
card: "summary_large_image",
title: `${username}'s DevTrack Profile`,
description: `GitHub stats and coding activity for ${username}`,
title,
description,
images: [ogImageUrl.toString()],
},
};
}

export default async function PublicProfilePage({
params,
}: {
Expand Down
Loading