Skip to content

Commit d4e6991

Browse files
committed
opengraph fixes
1 parent f40d1c2 commit d4e6991

File tree

12 files changed

+366
-135
lines changed

12 files changed

+366
-135
lines changed

src/lib/og-fonts.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Shared font loading for OG image generation
2+
async function loadFont(weight: number): Promise<ArrayBuffer> {
3+
const url = `https://fonts.googleapis.com/css2?family=Inter:wght@${weight}&display=swap`;
4+
const css = await fetch(url).then((r) => r.text());
5+
const match = css.match(/src: url\((.+?)\) format/);
6+
if (!match?.[1]) throw new Error(`Failed to load Inter ${weight}`);
7+
return fetch(match[1]).then((r) => r.arrayBuffer());
8+
}
9+
10+
let fontsPromise: Promise<
11+
{ name: string; data: ArrayBuffer; weight: 400 | 600 | 700; style: "normal" }[]
12+
> | null = null;
13+
14+
export function getFonts() {
15+
if (!fontsPromise) {
16+
fontsPromise = Promise.all([
17+
loadFont(400).then((data) => ({ name: "Inter" as const, data, weight: 400 as const, style: "normal" as const })),
18+
loadFont(600).then((data) => ({ name: "Inter" as const, data, weight: 600 as const, style: "normal" as const })),
19+
loadFont(700).then((data) => ({ name: "Inter" as const, data, weight: 700 as const, style: "normal" as const })),
20+
]);
21+
}
22+
return fontsPromise;
23+
}

src/lib/og-image.tsx

Lines changed: 184 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,18 @@ interface OgImageProps {
77
tags: string[];
88
}
99

10-
export function OgImage({ title, author, date, tags }: OgImageProps): ReactNode {
11-
// Truncate title if too long
12-
const displayTitle = title.length > 100 ? title.slice(0, 97) + "…" : title;
13-
const displayTags = tags.slice(0, 4);
10+
interface OgPageImageProps {
11+
title: string;
12+
subtitle: string;
13+
detail?: string;
14+
}
15+
16+
const pythonLogoBlue =
17+
"M126.916.072c-64.832 0-60.784 28.115-60.784 28.115l.072 29.128h61.868v8.745H41.631S.145 61.355.145 126.77c0 65.417 36.21 63.097 36.21 63.097h21.61v-30.356s-1.165-36.21 35.632-36.21h61.362s34.475.557 34.475-33.319V33.97S194.67.072 126.916.072zM92.802 19.66a11.12 11.12 0 0 1 11.13 11.13 11.12 11.12 0 0 1-11.13 11.13 11.12 11.12 0 0 1-11.13-11.13 11.12 11.12 0 0 1 11.13-11.13z";
18+
const pythonLogoYellow =
19+
"M128.757 254.126c64.832 0 60.784-28.115 60.784-28.115l-.072-29.127H127.6v-8.745h86.441s41.486 4.705 41.486-60.712c0-65.416-36.21-63.096-36.21-63.096h-21.61v30.355s1.165 36.21-35.632 36.21h-61.362s-34.475-.557-34.475 33.32v56.013s-5.235 33.897 62.518 33.897zm34.114-19.586a11.12 11.12 0 0 1-11.13-11.13 11.12 11.12 0 0 1 11.13-11.131 11.12 11.12 0 0 1 11.13 11.13 11.12 11.12 0 0 1-11.13 11.13z";
1420

21+
function OgShell({ children }: { children: ReactNode }): ReactNode {
1522
return (
1623
<div
1724
style={{
@@ -25,7 +32,7 @@ export function OgImage({ title, author, date, tags }: OgImageProps): ReactNode
2532
overflow: "hidden",
2633
}}
2734
>
28-
{/* Subtle geometric pattern — diagonal lines */}
35+
{/* Subtle geometric pattern */}
2936
<div
3037
style={{
3138
position: "absolute",
@@ -40,7 +47,6 @@ export function OgImage({ title, author, date, tags }: OgImageProps): ReactNode
4047
backgroundSize: "56px 56px",
4148
}}
4249
/>
43-
4450
{/* Top accent bar */}
4551
<div
4652
style={{
@@ -51,8 +57,63 @@ export function OgImage({ title, author, date, tags }: OgImageProps): ReactNode
5157
flexShrink: 0,
5258
}}
5359
/>
60+
{children}
61+
</div>
62+
);
63+
}
5464

55-
{/* Content area */}
65+
function SiteBadge(): ReactNode {
66+
return (
67+
<div style={{ display: "flex", alignItems: "center", gap: "10" }}>
68+
<div
69+
style={{
70+
width: "32",
71+
height: "32",
72+
borderRadius: "8",
73+
background: "linear-gradient(135deg, #306998, #4B8BBE)",
74+
display: "flex",
75+
alignItems: "center",
76+
justifyContent: "center",
77+
}}
78+
>
79+
<svg width="18" height="18" viewBox="0 0 256 255" xmlns="http://www.w3.org/2000/svg">
80+
<path d={pythonLogoBlue} fill="white" />
81+
<path d={pythonLogoYellow} fill="rgba(255,255,255,0.6)" />
82+
</svg>
83+
</div>
84+
<span
85+
style={{ fontSize: "20", fontWeight: 600, color: "#a1a1aa", letterSpacing: "-0.01em" }}
86+
>
87+
Python Insider
88+
</span>
89+
</div>
90+
);
91+
}
92+
93+
function LogoWatermark(): ReactNode {
94+
return (
95+
<div
96+
style={{
97+
position: "absolute",
98+
right: "40",
99+
bottom: "60",
100+
width: "280",
101+
height: "280",
102+
display: "flex",
103+
opacity: 0.06,
104+
}}
105+
>
106+
<svg viewBox="0 0 256 255" width="280" height="280" xmlns="http://www.w3.org/2000/svg">
107+
<path d={pythonLogoBlue} fill="#306998" />
108+
<path d={pythonLogoYellow} fill="#ffd43b" />
109+
</svg>
110+
</div>
111+
);
112+
}
113+
114+
export function OgPageImage({ title, subtitle, detail }: OgPageImageProps): ReactNode {
115+
return (
116+
<OgShell>
56117
<div
57118
style={{
58119
display: "flex",
@@ -63,91 +124,137 @@ export function OgImage({ title, author, date, tags }: OgImageProps): ReactNode
63124
position: "relative",
64125
}}
65126
>
66-
{/* Python logo watermark */}
127+
<LogoWatermark />
128+
<SiteBadge />
129+
130+
{/* Center: large title + subtitle */}
67131
<div
68132
style={{
69-
position: "absolute",
70-
right: "40",
71-
bottom: "60",
72-
width: "280",
73-
height: "280",
74133
display: "flex",
75-
opacity: 0.06,
134+
flexDirection: "column",
135+
flex: 1,
136+
justifyContent: "center",
137+
marginTop: "-16",
138+
gap: "16",
76139
}}
77140
>
78-
<svg
79-
viewBox="0 0 256 255"
80-
width="280"
81-
height="280"
82-
xmlns="http://www.w3.org/2000/svg"
141+
<div
142+
style={{
143+
fontSize: "64",
144+
fontWeight: 700,
145+
color: "#fafafa",
146+
lineHeight: 1.15,
147+
letterSpacing: "-0.03em",
148+
}}
83149
>
84-
<path
85-
d="M126.916.072c-64.832 0-60.784 28.115-60.784 28.115l.072 29.128h61.868v8.745H41.631S.145 61.355.145 126.77c0 65.417 36.21 63.097 36.21 63.097h21.61v-30.356s-1.165-36.21 35.632-36.21h61.362s34.475.557 34.475-33.319V33.97S194.67.072 126.916.072zM92.802 19.66a11.12 11.12 0 0 1 11.13 11.13 11.12 11.12 0 0 1-11.13 11.13 11.12 11.12 0 0 1-11.13-11.13 11.12 11.12 0 0 1 11.13-11.13z"
86-
fill="#306998"
87-
/>
88-
<path
89-
d="M128.757 254.126c64.832 0 60.784-28.115 60.784-28.115l-.072-29.127H127.6v-8.745h86.441s41.486 4.705 41.486-60.712c0-65.416-36.21-63.096-36.21-63.096h-21.61v30.355s1.165 36.21-35.632 36.21h-61.362s-34.475-.557-34.475 33.32v56.013s-5.235 33.897 62.518 33.897zm34.114-19.586a11.12 11.12 0 0 1-11.13-11.13 11.12 11.12 0 0 1 11.13-11.131 11.12 11.12 0 0 1 11.13 11.13 11.12 11.12 0 0 1-11.13 11.13z"
90-
fill="#ffd43b"
91-
/>
92-
</svg>
150+
{title}
151+
</div>
152+
<div
153+
style={{
154+
fontSize: "24",
155+
fontWeight: 400,
156+
color: "#a1a1aa",
157+
lineHeight: 1.4,
158+
maxWidth: "700",
159+
}}
160+
>
161+
{subtitle}
162+
</div>
93163
</div>
94164

95-
{/* Top section: site name */}
165+
{/* Bottom detail */}
166+
{detail && (
167+
<div
168+
style={{
169+
display: "flex",
170+
alignItems: "center",
171+
}}
172+
>
173+
<span style={{ fontSize: "18", color: "#71717a" }}>{detail}</span>
174+
</div>
175+
)}
176+
</div>
177+
</OgShell>
178+
);
179+
}
180+
181+
export function OgAuthorImage({
182+
name,
183+
postCount,
184+
}: {
185+
name: string;
186+
postCount: number;
187+
}): ReactNode {
188+
return (
189+
<OgShell>
190+
<div
191+
style={{
192+
display: "flex",
193+
flexDirection: "column",
194+
flex: 1,
195+
padding: "56px 64px 48px",
196+
justifyContent: "space-between",
197+
position: "relative",
198+
}}
199+
>
200+
<LogoWatermark />
201+
<SiteBadge />
202+
96203
<div
97204
style={{
98205
display: "flex",
99-
alignItems: "center",
100-
gap: "12",
206+
flexDirection: "column",
207+
flex: 1,
208+
justifyContent: "center",
209+
marginTop: "-16",
210+
gap: "16",
101211
}}
102212
>
213+
<div style={{ fontSize: "24", fontWeight: 400, color: "#71717a" }}>Author</div>
103214
<div
104215
style={{
105-
display: "flex",
106-
alignItems: "center",
107-
gap: "10",
216+
fontSize: "64",
217+
fontWeight: 700,
218+
color: "#fafafa",
219+
lineHeight: 1.15,
220+
letterSpacing: "-0.03em",
108221
}}
109222
>
110-
<div
111-
style={{
112-
width: "32",
113-
height: "32",
114-
borderRadius: "8",
115-
background: "linear-gradient(135deg, #306998, #4B8BBE)",
116-
display: "flex",
117-
alignItems: "center",
118-
justifyContent: "center",
119-
}}
120-
>
121-
<svg
122-
width="18"
123-
height="18"
124-
viewBox="0 0 256 255"
125-
xmlns="http://www.w3.org/2000/svg"
126-
>
127-
<path
128-
d="M126.916.072c-64.832 0-60.784 28.115-60.784 28.115l.072 29.128h61.868v8.745H41.631S.145 61.355.145 126.77c0 65.417 36.21 63.097 36.21 63.097h21.61v-30.356s-1.165-36.21 35.632-36.21h61.362s34.475.557 34.475-33.319V33.97S194.67.072 126.916.072zM92.802 19.66a11.12 11.12 0 0 1 11.13 11.13 11.12 11.12 0 0 1-11.13 11.13 11.12 11.12 0 0 1-11.13-11.13 11.12 11.12 0 0 1 11.13-11.13z"
129-
fill="white"
130-
/>
131-
<path
132-
d="M128.757 254.126c64.832 0 60.784-28.115 60.784-28.115l-.072-29.127H127.6v-8.745h86.441s41.486 4.705 41.486-60.712c0-65.416-36.21-63.096-36.21-63.096h-21.61v30.355s1.165 36.21-35.632 36.21h-61.362s-34.475-.557-34.475 33.32v56.013s-5.235 33.897 62.518 33.897zm34.114-19.586a11.12 11.12 0 0 1-11.13-11.13 11.12 11.12 0 0 1 11.13-11.131 11.12 11.12 0 0 1 11.13 11.13 11.12 11.12 0 0 1-11.13 11.13z"
133-
fill="rgba(255,255,255,0.6)"
134-
/>
135-
</svg>
136-
</div>
137-
<span
138-
style={{
139-
fontSize: "20",
140-
fontWeight: 600,
141-
color: "#a1a1aa",
142-
letterSpacing: "-0.01em",
143-
}}
144-
>
145-
Python Insider
146-
</span>
223+
{name}
147224
</div>
148225
</div>
149226

150-
{/* Middle section: title */}
227+
<div style={{ display: "flex", alignItems: "center", gap: "16" }}>
228+
<span style={{ fontSize: "18", fontWeight: 600, color: "#ffd43b" }}>
229+
{postCount} {postCount === 1 ? "post" : "posts"}
230+
</span>
231+
<span style={{ fontSize: "18", color: "#52525b" }}>·</span>
232+
<span style={{ fontSize: "18", color: "#71717a" }}>Python Insider</span>
233+
</div>
234+
</div>
235+
</OgShell>
236+
);
237+
}
238+
239+
export function OgImage({ title, author, date, tags }: OgImageProps): ReactNode {
240+
const displayTitle = title.length > 100 ? title.slice(0, 97) + "…" : title;
241+
const displayTags = tags.slice(0, 4);
242+
243+
return (
244+
<OgShell>
245+
<div
246+
style={{
247+
display: "flex",
248+
flexDirection: "column",
249+
flex: 1,
250+
padding: "56px 64px 48px",
251+
justifyContent: "space-between",
252+
position: "relative",
253+
}}
254+
>
255+
<LogoWatermark />
256+
<SiteBadge />
257+
151258
<div
152259
style={{
153260
display: "flex",
@@ -171,48 +278,21 @@ export function OgImage({ title, author, date, tags }: OgImageProps): ReactNode
171278
</div>
172279
</div>
173280

174-
{/* Bottom section: author, date, tags */}
175281
<div
176282
style={{
177283
display: "flex",
178284
alignItems: "center",
179285
justifyContent: "space-between",
180286
}}
181287
>
182-
<div
183-
style={{
184-
display: "flex",
185-
alignItems: "center",
186-
gap: "16",
187-
}}
188-
>
189-
<span
190-
style={{
191-
fontSize: "18",
192-
fontWeight: 600,
193-
color: "#ffd43b",
194-
}}
195-
>
196-
{author}
197-
</span>
288+
<div style={{ display: "flex", alignItems: "center", gap: "16" }}>
289+
<span style={{ fontSize: "18", fontWeight: 600, color: "#ffd43b" }}>{author}</span>
198290
<span style={{ fontSize: "18", color: "#52525b" }}>·</span>
199-
<span
200-
style={{
201-
fontSize: "18",
202-
color: "#71717a",
203-
}}
204-
>
205-
{date}
206-
</span>
291+
<span style={{ fontSize: "18", color: "#71717a" }}>{date}</span>
207292
</div>
208293

209294
{displayTags.length > 0 && (
210-
<div
211-
style={{
212-
display: "flex",
213-
gap: "8",
214-
}}
215-
>
295+
<div style={{ display: "flex", gap: "8" }}>
216296
{displayTags.map((tag) => (
217297
<div
218298
key={tag}
@@ -233,6 +313,6 @@ export function OgImage({ title, author, date, tags }: OgImageProps): ReactNode
233313
)}
234314
</div>
235315
</div>
236-
</div>
316+
</OgShell>
237317
);
238318
}

0 commit comments

Comments
 (0)