Skip to content

Commit 177c55f

Browse files
sanzzzz-gSANCHI GOYAL
andauthored
feat(og): Implement dynamic social sharing OG images (JhaSourav07#101)
* Refactor GET function for user streak data Refactor GET function to fetch user streak data and generate OG image response. * Enhance metadata for Open Graph and Twitter Added Open Graph and Twitter metadata for better sharing. * Refactor GET function in route.tsx Refactor GET function to improve readability and structure. Updated fetch call and removed unnecessary comments. * Remove commit and streak display elements Removed several div elements displaying total commits, longest streak, current streak, and branding information. * Update layout.tsx * Update route.tsx * Update route.tsx * style: fix prettier formatting * feat(og): implement dynamic social sharing OG images --------- Co-authored-by: SANCHI GOYAL <sanchigoyal@Mac.lan> Reviewed by - jhasourav07
1 parent 08d8951 commit 177c55f

2 files changed

Lines changed: 164 additions & 146 deletions

File tree

app/api/og/route.tsx

Lines changed: 143 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -1,198 +1,196 @@
11
import { ImageResponse } from 'next/og';
22
import { NextRequest } from 'next/server';
33

4-
// Must be Edge runtime — Vercel's OG image generation requires it,
5-
// and it's much faster than Node (no cold-start overhead).
64
export const runtime = 'edge';
75

8-
// X/Twitter crawlers have a ~5 s hard timeout.
9-
// We use the lightweight REST endpoint (no GraphQL, no rate-limit headers)
10-
// and fall back gracefully so the image always renders.
11-
async function fetchUserFast(username: string) {
12-
const headers: HeadersInit = { Accept: 'application/vnd.github+json' };
13-
const pat = process.env.GITHUB_PAT;
14-
if (pat) headers.Authorization = `Bearer ${pat}`;
15-
16-
const res = await fetch(`https://api.github.com/users/${username}`, {
17-
headers,
18-
// next: { revalidate: 3600 }, // not supported on edge — use Cache-Control header instead
19-
});
20-
21-
if (!res.ok) return null;
22-
return res.json() as Promise<{
23-
name: string | null;
24-
login: string;
25-
avatar_url: string;
26-
bio: string | null;
27-
public_repos: number;
28-
followers: number;
29-
}>;
30-
}
6+
export async function GET(req: NextRequest) {
7+
const { searchParams } = new URL(req.url);
8+
const user = searchParams.get('user') ?? 'unknown';
9+
10+
let totalCommits = 0;
11+
let longestStreak = 0;
12+
let currentStreak = 0;
3113

32-
export async function GET(request: NextRequest) {
33-
const { searchParams } = new URL(request.url);
34-
const username = searchParams.get('username') ?? 'ghost';
14+
try {
15+
const baseUrl = req.nextUrl.origin;
3516

36-
// Fetch with a hard 4 s budget so we never exceed X's timeout
37-
const user = await Promise.race([
38-
fetchUserFast(username),
39-
new Promise<null>((res) => setTimeout(() => res(null), 4000)),
40-
]);
17+
const res = await fetch(`${baseUrl}/api/streak?user=${user}&refresh=true`, {
18+
cache: 'no-store',
19+
});
4120

42-
const name = user?.name || username;
43-
const avatarUrl = user?.avatar_url ?? `https://github.com/${username}.png`;
44-
const bio = (user?.bio ?? '').slice(0, 90);
45-
const repos = user?.public_repos ?? 0;
46-
const followers = user?.followers ?? 0;
21+
if (res.ok) {
22+
const data = (await res.json()) as {
23+
totalContributions?: number;
24+
longestStreak?: number;
25+
currentStreak?: number;
26+
};
27+
28+
totalCommits = data.totalContributions ?? 0;
29+
longestStreak = data.longestStreak ?? 0;
30+
currentStreak = data.currentStreak ?? 0;
31+
}
32+
} catch {
33+
// fallback
34+
}
4735

4836
return new ImageResponse(
4937
<div
5038
style={{
5139
width: '1200px',
5240
height: '630px',
53-
background: '#000000',
41+
background: '#0d1117',
5442
display: 'flex',
5543
flexDirection: 'column',
5644
alignItems: 'center',
5745
justifyContent: 'center',
58-
fontFamily: 'ui-sans-serif, system-ui, sans-serif',
46+
fontFamily: 'sans-serif',
5947
position: 'relative',
60-
overflow: 'hidden',
6148
}}
6249
>
63-
{/* Subtle top border glow */}
6450
<div
6551
style={{
6652
position: 'absolute',
67-
top: 0,
68-
left: 0,
69-
right: 0,
70-
height: '1px',
71-
background: 'rgba(255,255,255,0.12)',
53+
width: '600px',
54+
height: '300px',
55+
background: 'radial-gradient(ellipse, #58a6ff22 0%, transparent 70%)',
56+
top: '50px',
57+
left: '300px',
7258
}}
7359
/>
7460

75-
{/* Faint radial glow behind card */}
7661
<div
7762
style={{
78-
position: 'absolute',
79-
top: '50%',
80-
left: '50%',
81-
transform: 'translate(-50%, -50%)',
82-
width: '700px',
83-
height: '700px',
84-
borderRadius: '50%',
85-
background: 'radial-gradient(circle, rgba(99,102,241,0.08) 0%, transparent 70%)',
63+
fontSize: '48px',
64+
color: '#58a6ff',
65+
fontWeight: 'bold',
66+
marginBottom: '24px',
8667
}}
87-
/>
68+
>
69+
⚡ CommitPulse
70+
</div>
71+
72+
<div
73+
style={{
74+
fontSize: '32px',
75+
color: '#c9d1d9',
76+
marginBottom: '48px',
77+
}}
78+
>
79+
@{user}
80+
</div>
8881

89-
{/* Card */}
9082
<div
9183
style={{
9284
display: 'flex',
93-
flexDirection: 'column',
94-
alignItems: 'center',
95-
gap: '28px',
96-
padding: '56px 80px',
97-
background: 'rgba(255,255,255,0.03)',
98-
border: '1px solid rgba(255,255,255,0.09)',
99-
borderRadius: '24px',
100-
width: '920px',
85+
gap: '48px',
10186
}}
10287
>
103-
{/* Avatar + name */}
104-
<div style={{ display: 'flex', alignItems: 'center', gap: '36px' }}>
105-
{/* eslint-disable-next-line @next/next/no-img-element */}
106-
<img
107-
src={avatarUrl}
108-
width={104}
109-
height={104}
110-
style={{ borderRadius: '50%', border: '1px solid rgba(255,255,255,0.15)' }}
111-
alt={name}
112-
/>
113-
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
114-
<span style={{ color: '#ffffff', fontSize: '40px', fontWeight: 700, lineHeight: 1 }}>
115-
{name}
116-
</span>
117-
<span style={{ color: '#A1A1AA', fontSize: '20px', fontWeight: 400 }}>@{username}</span>
118-
{bio && (
119-
<span style={{ color: 'rgba(255,255,255,0.45)', fontSize: '15px', marginTop: '2px' }}>
120-
{bio}
121-
</span>
122-
)}
88+
<div
89+
style={{
90+
display: 'flex',
91+
flexDirection: 'column',
92+
alignItems: 'center',
93+
background: '#161b22',
94+
border: '1px solid #30363d',
95+
borderRadius: '16px',
96+
padding: '32px 48px',
97+
}}
98+
>
99+
<div
100+
style={{
101+
fontSize: '56px',
102+
fontWeight: 'bold',
103+
color: '#58a6ff',
104+
}}
105+
>
106+
{totalCommits}
107+
</div>
108+
109+
<div
110+
style={{
111+
fontSize: '18px',
112+
color: '#8b949e',
113+
marginTop: '8px',
114+
}}
115+
>
116+
Total Commits
117+
</div>
118+
</div>
119+
120+
<div
121+
style={{
122+
display: 'flex',
123+
flexDirection: 'column',
124+
alignItems: 'center',
125+
background: '#161b22',
126+
border: '1px solid #30363d',
127+
borderRadius: '16px',
128+
padding: '32px 48px',
129+
}}
130+
>
131+
<div
132+
style={{
133+
fontSize: '56px',
134+
fontWeight: 'bold',
135+
color: '#f78166',
136+
}}
137+
>
138+
{longestStreak}
139+
</div>
140+
141+
<div
142+
style={{
143+
fontSize: '18px',
144+
color: '#8b949e',
145+
marginTop: '8px',
146+
}}
147+
>
148+
Longest Streak 🔥
123149
</div>
124150
</div>
125151

126-
{/* Divider */}
127152
<div
128153
style={{
129-
width: '100%',
130-
height: '1px',
131-
background: 'rgba(255,255,255,0.08)',
154+
display: 'flex',
155+
flexDirection: 'column',
156+
alignItems: 'center',
157+
background: '#161b22',
158+
border: '1px solid #30363d',
159+
borderRadius: '16px',
160+
padding: '32px 48px',
132161
}}
133-
/>
134-
135-
{/* Stats */}
136-
<div style={{ display: 'flex', gap: '56px', alignItems: 'center' }}>
137-
{[
138-
{ value: repos, label: 'Repositories' },
139-
{ value: followers, label: 'Followers' },
140-
].map((stat, i, arr) => (
141-
<div key={stat.label} style={{ display: 'flex', alignItems: 'center', gap: '56px' }}>
142-
<div
143-
style={{
144-
display: 'flex',
145-
flexDirection: 'column',
146-
alignItems: 'center',
147-
gap: '6px',
148-
}}
149-
>
150-
<span
151-
style={{ fontSize: '52px', fontWeight: 700, color: '#ffffff', lineHeight: 1 }}
152-
>
153-
{stat.value.toLocaleString()}
154-
</span>
155-
<span
156-
style={{
157-
fontSize: '12px',
158-
color: '#A1A1AA',
159-
letterSpacing: '2px',
160-
textTransform: 'uppercase',
161-
}}
162-
>
163-
{stat.label}
164-
</span>
165-
</div>
166-
{i < arr.length - 1 && (
167-
<div
168-
style={{ width: '1px', height: '56px', background: 'rgba(255,255,255,0.08)' }}
169-
/>
170-
)}
171-
</div>
172-
))}
162+
>
163+
<div
164+
style={{
165+
fontSize: '56px',
166+
fontWeight: 'bold',
167+
color: '#3fb950',
168+
}}
169+
>
170+
{currentStreak}
171+
</div>
172+
173+
<div
174+
style={{
175+
fontSize: '18px',
176+
color: '#8b949e',
177+
marginTop: '8px',
178+
}}
179+
>
180+
Current Streak ⚡
181+
</div>
173182
</div>
174183
</div>
175184

176-
{/* Branding */}
177185
<div
178186
style={{
179187
position: 'absolute',
180-
bottom: '28px',
181-
display: 'flex',
182-
alignItems: 'center',
183-
gap: '8px',
188+
bottom: '32px',
189+
fontSize: '16px',
190+
color: '#484f58',
184191
}}
185192
>
186-
<span
187-
style={{
188-
fontSize: '13px',
189-
color: 'rgba(255,255,255,0.25)',
190-
letterSpacing: '3px',
191-
textTransform: 'uppercase',
192-
}}
193-
>
194-
commitpulse.vercel.app
195-
</span>
193+
commitpulse.vercel.app
196194
</div>
197195
</div>,
198196
{

app/layout.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// app/layout.tsx
21
import './globals.css';
32
import { Inter } from 'next/font/google';
43
import { Analytics } from '@vercel/analytics/next';
@@ -10,6 +9,27 @@ const inter = Inter({ subsets: ['latin'] });
109
export const metadata = {
1110
title: 'CommitPulse | Visualize Your Rhythm',
1211
description: 'Premium GitHub streak monoliths',
12+
openGraph: {
13+
title: 'CommitPulse | Visualize Your Rhythm',
14+
description: 'Your GitHub contributions as a cinematic SVG monolith.',
15+
url: 'https://commitpulse.vercel.app',
16+
siteName: 'CommitPulse',
17+
images: [
18+
{
19+
url: '/api/og?user=jhasourav07',
20+
width: 1200,
21+
height: 630,
22+
alt: 'CommitPulse Preview',
23+
},
24+
],
25+
type: 'website',
26+
},
27+
twitter: {
28+
card: 'summary_large_image',
29+
title: 'CommitPulse | Visualize Your Rhythm',
30+
description: 'Your GitHub contributions as a cinematic SVG monolith.',
31+
images: ['/api/og?user=jhasourav07'],
32+
},
1333
};
1434

1535
export default function RootLayout({ children }: { children: React.ReactNode }) {

0 commit comments

Comments
 (0)