Skip to content

Commit 34d3d78

Browse files
feat: generate og images
1 parent ac8bb62 commit 34d3d78

File tree

2 files changed

+244
-2
lines changed

2 files changed

+244
-2
lines changed
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import fs from 'fs/promises'
2+
import path from 'path'
3+
import { ImageResponse } from 'next/og'
4+
import type { NextRequest } from 'next/server'
5+
import { getPostBySlug } from '@/lib/blog/registry'
6+
import { getPrimaryCategory } from '@/app/(landing)/studio/tag-colors'
7+
8+
export const revalidate = 3600
9+
10+
function getTitleFontSize(title: string): number {
11+
if (title.length > 80) return 36
12+
if (title.length > 60) return 40
13+
if (title.length > 40) return 48
14+
return 56
15+
}
16+
17+
function formatDate(iso: string): string {
18+
return new Date(iso).toLocaleDateString('en-US', {
19+
month: 'short',
20+
day: 'numeric',
21+
year: 'numeric',
22+
})
23+
}
24+
25+
export async function GET(request: NextRequest) {
26+
const slug = request.nextUrl.searchParams.get('slug')
27+
28+
if (!slug) {
29+
return new Response('Missing slug parameter', { status: 400 })
30+
}
31+
32+
let post
33+
try {
34+
post = await getPostBySlug(slug)
35+
} catch {
36+
return new Response('Post not found', { status: 404 })
37+
}
38+
39+
const category = getPrimaryCategory(post.tags)
40+
const authors = post.authors && post.authors.length > 0 ? post.authors : [post.author]
41+
const authorNames = authors.map((a) => a.name).join(', ')
42+
43+
const fontsDir = path.join(process.cwd(), 'app', '_styles', 'fonts', 'season')
44+
const [fontMedium, fontBold] = await Promise.all([
45+
fs.readFile(path.join(fontsDir, 'SeasonSans-Medium.woff')),
46+
fs.readFile(path.join(fontsDir, 'SeasonSans-Bold.woff')),
47+
])
48+
49+
const COLORS = ['#2ABBF8', '#FA4EDF', '#FFCC02', '#00F701'] as const
50+
51+
return new ImageResponse(
52+
<div
53+
style={{
54+
height: '100%',
55+
width: '100%',
56+
display: 'flex',
57+
flexDirection: 'column',
58+
justifyContent: 'space-between',
59+
padding: '56px 64px',
60+
background: '#1C1C1C',
61+
fontFamily: 'Season Sans',
62+
position: 'relative',
63+
overflow: 'hidden',
64+
}}
65+
>
66+
<div
67+
style={{
68+
position: 'absolute',
69+
top: 0,
70+
left: 0,
71+
right: 0,
72+
bottom: 0,
73+
backgroundImage:
74+
'radial-gradient(circle at 2px 2px, rgba(255, 255, 255, 0.03) 1px, transparent 0)',
75+
backgroundSize: '32px 32px',
76+
}}
77+
/>
78+
<div
79+
style={{
80+
position: 'absolute',
81+
top: 0,
82+
left: 0,
83+
right: 0,
84+
bottom: 0,
85+
border: '1px solid #2A2A2A',
86+
}}
87+
/>
88+
<div
89+
style={{
90+
display: 'flex',
91+
flexDirection: 'column',
92+
position: 'absolute',
93+
top: 0,
94+
left: 0,
95+
}}
96+
>
97+
<div style={{ display: 'flex' }}>
98+
{COLORS.map((color) => (
99+
<div key={color} style={{ width: 16, height: 16, backgroundColor: color }} />
100+
))}
101+
</div>
102+
<div style={{ display: 'flex', flexDirection: 'column' }}>
103+
{COLORS.slice(0, 3).map((color) => (
104+
<div key={`v-${color}`} style={{ width: 16, height: 16, backgroundColor: color }} />
105+
))}
106+
</div>
107+
</div>
108+
<div
109+
style={{
110+
display: 'flex',
111+
position: 'absolute',
112+
bottom: 0,
113+
right: 0,
114+
}}
115+
>
116+
{[...COLORS].reverse().map((color) => (
117+
<div key={`b-${color}`} style={{ width: 16, height: 16, backgroundColor: color }} />
118+
))}
119+
</div>
120+
<div style={{ display: 'flex', alignItems: 'center', gap: 16, zIndex: 1 }}>
121+
<div
122+
style={{
123+
display: 'flex',
124+
alignItems: 'center',
125+
padding: '4px 12px',
126+
backgroundColor: category.color,
127+
color: '#000000',
128+
fontSize: 12,
129+
fontWeight: 700,
130+
textTransform: 'uppercase',
131+
letterSpacing: '0.1em',
132+
}}
133+
>
134+
{category.label}
135+
</div>
136+
{post.readingTime && (
137+
<span
138+
style={{
139+
fontSize: 13,
140+
color: '#666666',
141+
textTransform: 'uppercase',
142+
letterSpacing: '0.08em',
143+
fontWeight: 500,
144+
}}
145+
>
146+
{post.readingTime} min read
147+
</span>
148+
)}
149+
</div>
150+
<div
151+
style={{
152+
display: 'flex',
153+
flexDirection: 'column',
154+
gap: 16,
155+
zIndex: 1,
156+
flex: 1,
157+
justifyContent: 'center',
158+
}}
159+
>
160+
<div
161+
style={{
162+
fontSize: getTitleFontSize(post.title),
163+
fontWeight: 500,
164+
color: '#ECECEC',
165+
lineHeight: 1.15,
166+
letterSpacing: '-0.02em',
167+
maxWidth: '90%',
168+
}}
169+
>
170+
{post.title}
171+
</div>
172+
<div
173+
style={{
174+
fontSize: 18,
175+
color: '#999999',
176+
lineHeight: 1.5,
177+
maxWidth: '80%',
178+
overflow: 'hidden',
179+
textOverflow: 'ellipsis',
180+
}}
181+
>
182+
{post.description.length > 140
183+
? `${post.description.slice(0, 140)}...`
184+
: post.description}
185+
</div>
186+
</div>
187+
<div
188+
style={{
189+
display: 'flex',
190+
justifyContent: 'space-between',
191+
alignItems: 'center',
192+
zIndex: 1,
193+
borderTop: '1px solid #2A2A2A',
194+
paddingTop: 24,
195+
}}
196+
>
197+
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
198+
<span style={{ fontSize: 14, color: '#999999', fontWeight: 500 }}>{authorNames}</span>
199+
<span
200+
style={{
201+
width: 4,
202+
height: 4,
203+
backgroundColor: '#3d3d3d',
204+
borderRadius: '50%',
205+
}}
206+
/>
207+
<span style={{ fontSize: 14, color: '#666666', fontWeight: 500 }}>
208+
{formatDate(post.date)}
209+
</span>
210+
</div>
211+
<span
212+
style={{
213+
fontSize: 14,
214+
color: '#666666',
215+
fontWeight: 500,
216+
letterSpacing: '0.05em',
217+
}}
218+
>
219+
sim.ai/studio
220+
</span>
221+
</div>
222+
</div>,
223+
{
224+
width: 1200,
225+
height: 630,
226+
fonts: [
227+
{
228+
name: 'Season Sans',
229+
data: fontMedium,
230+
style: 'normal' as const,
231+
weight: 500 as const,
232+
},
233+
{
234+
name: 'Season Sans',
235+
data: fontBold,
236+
style: 'normal' as const,
237+
weight: 700 as const,
238+
},
239+
],
240+
}
241+
)
242+
}

apps/sim/lib/blog/seo.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function buildPostMetadata(post: BlogMeta): Metadata {
3333
tags: post.tags,
3434
images: [
3535
{
36-
url: post.ogImage.startsWith('http') ? post.ogImage : `${baseUrl}${post.ogImage}`,
36+
url: `${baseUrl}/studio/og?slug=${encodeURIComponent(post.slug)}`,
3737
width: 1200,
3838
height: 630,
3939
alt: post.ogAlt || post.title,
@@ -44,7 +44,7 @@ export function buildPostMetadata(post: BlogMeta): Metadata {
4444
card: 'summary_large_image',
4545
title: post.title,
4646
description: post.description,
47-
images: [post.ogImage],
47+
images: [`${baseUrl}/studio/og?slug=${encodeURIComponent(post.slug)}`],
4848
creator: post.author.url?.includes('x.com') ? `@${post.author.xHandle || ''}` : undefined,
4949
site: '@simdotai',
5050
},

0 commit comments

Comments
 (0)