1- import fs from 'fs/promises'
2- import path from 'path'
1+ import { readFile } from 'node: fs/promises'
2+ import { join } from 'node: path'
33import { ImageResponse } from 'next/og'
44import type { NextRequest } from 'next/server'
55import { getPostMetaBySlug } from '@/lib/blog/registry'
@@ -8,13 +8,124 @@ import { getPrimaryCategory } from '@/app/(landing)/blog/tag-colors'
88
99export const runtime = 'nodejs'
1010
11+ async function getLogoDataUrl ( ) : Promise < string > {
12+ const logoPath = join ( process . cwd ( ) , 'public' , 'logo' , 'sim-landing.svg' )
13+ const buffer = await readFile ( logoPath )
14+ return `data:image/svg+xml;base64,${ buffer . toString ( 'base64' ) } `
15+ }
16+
1117function getTitleFontSize ( title : string ) : number {
1218 if ( title . length > 80 ) return 36
1319 if ( title . length > 60 ) return 40
1420 if ( title . length > 40 ) return 48
1521 return 56
1622}
1723
24+ async function loadGoogleFont ( font : string , weights : string , text : string ) : Promise < ArrayBuffer > {
25+ const url = `https://fonts.googleapis.com/css2?family=${ font } :wght@${ weights } &text=${ encodeURIComponent ( text ) } `
26+ const css = await ( await fetch ( url ) ) . text ( )
27+ const resource = css . match ( / s r c : u r l \( ( .+ ) \) f o r m a t \( ' ( o p e n t y p e | t r u e t y p e ) ' \) / )
28+
29+ if ( resource ) {
30+ const response = await fetch ( resource [ 1 ] )
31+ if ( response . status === 200 ) {
32+ return await response . arrayBuffer ( )
33+ }
34+ }
35+
36+ throw new Error ( 'Failed to load font data' )
37+ }
38+
39+ function Block ( {
40+ x,
41+ y,
42+ w,
43+ h,
44+ color,
45+ opacity = 1 ,
46+ } : {
47+ x : number
48+ y : number
49+ w : number
50+ h : number
51+ color : string
52+ opacity ?: number
53+ } ) {
54+ return (
55+ < div
56+ style = { {
57+ position : 'absolute' ,
58+ left : x ,
59+ top : y ,
60+ width : w ,
61+ height : h ,
62+ borderRadius : 2.6 ,
63+ backgroundColor : color ,
64+ opacity,
65+ } }
66+ />
67+ )
68+ }
69+
70+ function BlocksLeft ( ) {
71+ return (
72+ < div style = { { display : 'flex' , position : 'relative' , width : 34 , height : 226 } } >
73+ < Block x = { 0 } y = { 0 } w = { 34 } h = { 34 } color = '#FA4EDF' opacity = { 0.6 } />
74+ < Block x = { 0 } y = { 0 } w = { 17 } h = { 17 } color = '#FA4EDF' />
75+ < Block x = { 17 } y = { 0 } w = { 17 } h = { 68 } color = '#FA4EDF' opacity = { 0.6 } />
76+ < Block x = { 17 } y = { 17 } w = { 17 } h = { 17 } color = '#FA4EDF' />
77+ < Block x = { 0 } y = { 52 } w = { 34 } h = { 17 } color = '#FA4EDF' opacity = { 0.6 } />
78+ < Block x = { 17 } y = { 85 } w = { 17 } h = { 141 } color = '#00F701' opacity = { 0.6 } />
79+ < Block x = { 0 } y = { 120 } w = { 17 } h = { 17 } color = '#FFCC02' />
80+ < Block x = { 0 } y = { 120 } w = { 17 } h = { 34 } color = '#FFCC02' opacity = { 0.4 } />
81+ < Block x = { 0 } y = { 154 } w = { 17 } h = { 17 } color = '#00F701' />
82+ < Block x = { 0 } y = { 154 } w = { 34 } h = { 34 } color = '#00F701' opacity = { 0.5 } />
83+ </ div >
84+ )
85+ }
86+
87+ function BlocksRight ( ) {
88+ return (
89+ < div style = { { display : 'flex' , position : 'relative' , width : 34 , height : 205 } } >
90+ < Block x = { 0 } y = { 0 } w = { 17 } h = { 17 } color = '#FA4EDF' opacity = { 0.6 } />
91+ < Block x = { 17 } y = { 0 } w = { 17 } h = { 17 } color = '#FA4EDF' opacity = { 0.6 } />
92+ < Block x = { 17 } y = { 0 } w = { 34 } h = { 17 } color = '#FA4EDF' opacity = { 0.6 } />
93+ < Block x = { 17 } y = { 17 } w = { 17 } h = { 68 } color = '#FA4EDF' opacity = { 0.6 } />
94+ < Block x = { 17 } y = { 34 } w = { 17 } h = { 17 } color = '#FA4EDF' />
95+ < Block x = { 0 } y = { 34 } w = { 34 } h = { 17 } color = '#FA4EDF' opacity = { 0.6 } />
96+ < Block x = { 0 } y = { 69 } w = { 34 } h = { 17 } color = '#FA4EDF' opacity = { 0.6 } />
97+ < Block x = { 17 } y = { 102 } w = { 17 } h = { 102 } color = '#2ABBF8' opacity = { 0.6 } />
98+ < Block x = { 0 } y = { 137 } w = { 17 } h = { 17 } color = '#00F701' />
99+ < Block x = { 0 } y = { 137 } w = { 17 } h = { 34 } color = '#00F701' opacity = { 0.4 } />
100+ </ div >
101+ )
102+ }
103+
104+ function BlocksTopRight ( ) {
105+ return (
106+ < div style = { { display : 'flex' , position : 'relative' , width : 295 , height : 34 } } >
107+ < Block x = { 0 } y = { 0 } w = { 17 } h = { 34 } color = '#2ABBF8' />
108+ < Block x = { 0 } y = { 0 } w = { 17 } h = { 17 } color = '#2ABBF8' />
109+ < Block x = { 0 } y = { 0 } w = { 85 } h = { 17 } color = '#2ABBF8' opacity = { 0.6 } />
110+ < Block x = { 34 } y = { 0 } w = { 34 } h = { 34 } color = '#2ABBF8' opacity = { 0.6 } />
111+ < Block x = { 34 } y = { 0 } w = { 17 } h = { 17 } color = '#2ABBF8' />
112+ < Block x = { 52 } y = { 17 } w = { 17 } h = { 17 } color = '#2ABBF8' />
113+ < Block x = { 68 } y = { 0 } w = { 55 } h = { 17 } color = '#00F701' />
114+ < Block x = { 106 } y = { 0 } w = { 34 } h = { 34 } color = '#00F701' opacity = { 0.6 } />
115+ < Block x = { 106 } y = { 0 } w = { 51 } h = { 17 } color = '#00F701' opacity = { 0.6 } />
116+ < Block x = { 124 } y = { 17 } w = { 17 } h = { 17 } color = '#00F701' />
117+ < Block x = { 157 } y = { 0 } w = { 34 } h = { 17 } color = '#FFCC02' opacity = { 0.6 } />
118+ < Block x = { 157 } y = { 0 } w = { 17 } h = { 17 } color = '#FFCC02' />
119+ < Block x = { 209 } y = { 0 } w = { 17 } h = { 34 } color = '#FA4EDF' opacity = { 0.6 } />
120+ < Block x = { 209 } y = { 0 } w = { 68 } h = { 17 } color = '#FA4EDF' opacity = { 0.6 } />
121+ < Block x = { 243 } y = { 0 } w = { 34 } h = { 34 } color = '#FA4EDF' opacity = { 0.6 } />
122+ < Block x = { 243 } y = { 0 } w = { 17 } h = { 17 } color = '#FA4EDF' />
123+ < Block x = { 260 } y = { 0 } w = { 34 } h = { 17 } color = '#FA4EDF' opacity = { 0.6 } />
124+ < Block x = { 261 } y = { 17 } w = { 17 } h = { 17 } color = '#FA4EDF' />
125+ </ div >
126+ )
127+ }
128+
18129export async function GET ( request : NextRequest ) {
19130 const slug = request . nextUrl . searchParams . get ( 'slug' )
20131
@@ -32,36 +143,11 @@ export async function GET(request: NextRequest) {
32143 const authors = post . authors && post . authors . length > 0 ? post . authors : [ post . author ]
33144 const authorNames = authors . map ( ( a ) => a . name ) . join ( ', ' )
34145
35- let fontMedium : Buffer
36- let fontBold : Buffer
37- try {
38- const fontsDirPrimary = path . join ( process . cwd ( ) , 'app' , '_styles' , 'fonts' , 'season' )
39- const fontsDirFallback = path . join (
40- process . cwd ( ) ,
41- 'apps' ,
42- 'sim' ,
43- 'app' ,
44- '_styles' ,
45- 'fonts' ,
46- 'season'
47- )
48-
49- let fontsDir = fontsDirPrimary
50- try {
51- await fs . access ( fontsDirPrimary )
52- } catch {
53- fontsDir = fontsDirFallback
54- }
55-
56- ; [ fontMedium , fontBold ] = await Promise . all ( [
57- fs . readFile ( path . join ( fontsDir , 'SeasonSans-Medium.woff' ) ) ,
58- fs . readFile ( path . join ( fontsDir , 'SeasonSans-Bold.woff' ) ) ,
59- ] )
60- } catch {
61- return new Response ( 'Font assets not found' , { status : 500 } )
62- }
63-
64- const COLORS = [ '#5fc5ff' , '#F472B6' , '#fcd34d' , '#4BDE80' , '#FF8533' ] as const
146+ const allText = `${ category . label } ${ post . readingTime ? `${ post . readingTime } min read` : '' } ${ post . title } ${ post . description } ${ authorNames } ${ formatDate ( new Date ( post . date ) ) } sim.ai/blog`
147+ const [ fontData , logoDataUrl ] = await Promise . all ( [
148+ loadGoogleFont ( 'Inter' , '400;500;700' , allText ) ,
149+ getLogoDataUrl ( ) ,
150+ ] )
65151
66152 return new ImageResponse (
67153 < div
@@ -73,7 +159,7 @@ export async function GET(request: NextRequest) {
73159 justifyContent : 'space-between' ,
74160 padding : '56px 64px' ,
75161 background : '#1C1C1C' ,
76- fontFamily : 'Season Sans ' ,
162+ fontFamily : 'Inter ' ,
77163 position : 'relative' ,
78164 overflow : 'hidden' ,
79165 } }
@@ -100,68 +186,45 @@ export async function GET(request: NextRequest) {
100186 border : '1px solid #2A2A2A' ,
101187 } }
102188 />
189+
103190 < div
104191 style = { {
105- display : 'flex' ,
106- flexDirection : 'column' ,
107192 position : 'absolute' ,
108- top : 0 ,
109- left : 0 ,
193+ left : 96 ,
194+ top : 502 ,
195+ display : 'flex' ,
196+ transform : 'rotate(90deg)' ,
110197 } }
111198 >
112- < div style = { { display : 'flex' } } >
113- { COLORS . map ( ( color ) => (
114- < div key = { color } style = { { width : 16 , height : 16 , backgroundColor : color } } />
115- ) ) }
116- </ div >
117- < div style = { { display : 'flex' , flexDirection : 'column' } } >
118- { COLORS . slice ( 0 , 3 ) . map ( ( color ) => (
119- < div key = { `v-${ color } ` } style = { { width : 16 , height : 16 , backgroundColor : color } } />
120- ) ) }
121- </ div >
199+ < BlocksLeft />
122200 </ div >
201+
123202 < div
124203 style = { {
204+ position : 'absolute' ,
205+ right : 0 ,
206+ top : 212 ,
125207 display : 'flex' ,
208+ } }
209+ >
210+ < BlocksRight />
211+ </ div >
212+
213+ < div
214+ style = { {
126215 position : 'absolute' ,
127- bottom : 0 ,
128216 right : 0 ,
217+ top : 0 ,
218+ display : 'flex' ,
129219 } }
130220 >
131- { [ ...COLORS ] . reverse ( ) . map ( ( color ) => (
132- < div key = { `b-${ color } ` } style = { { width : 16 , height : 16 , backgroundColor : color } } />
133- ) ) }
221+ < BlocksTopRight />
134222 </ div >
135- < div style = { { display : 'flex' , alignItems : 'center' , gap : 16 , zIndex : 1 } } >
136- < div
137- style = { {
138- display : 'flex' ,
139- alignItems : 'center' ,
140- padding : '4px 12px' ,
141- backgroundColor : category . color ,
142- color : '#000000' ,
143- fontSize : 12 ,
144- fontWeight : 700 ,
145- textTransform : 'uppercase' ,
146- letterSpacing : '0.1em' ,
147- } }
148- >
149- { category . label }
150- </ div >
151- { post . readingTime && (
152- < span
153- style = { {
154- fontSize : 13 ,
155- color : '#666666' ,
156- textTransform : 'uppercase' ,
157- letterSpacing : '0.08em' ,
158- fontWeight : 500 ,
159- } }
160- >
161- { post . readingTime } min read
162- </ span >
163- ) }
223+
224+ < div style = { { display : 'flex' , flexDirection : 'column' , gap : 30 , zIndex : 1 } } >
225+ < img src = { logoDataUrl } alt = 'Sim' height = { 33 } width = { 106.5 } />
164226 </ div >
227+
165228 < div
166229 style = { {
167230 display : 'flex' ,
@@ -172,6 +235,36 @@ export async function GET(request: NextRequest) {
172235 justifyContent : 'center' ,
173236 } }
174237 >
238+ < div style = { { display : 'flex' , alignItems : 'center' , gap : 16 } } >
239+ < div
240+ style = { {
241+ display : 'flex' ,
242+ alignItems : 'center' ,
243+ padding : '4px 12px' ,
244+ backgroundColor : category . color ,
245+ color : '#000000' ,
246+ fontSize : 12 ,
247+ fontWeight : 700 ,
248+ textTransform : 'uppercase' ,
249+ letterSpacing : '0.1em' ,
250+ } }
251+ >
252+ { category . label }
253+ </ div >
254+ { post . readingTime && (
255+ < span
256+ style = { {
257+ fontSize : 13 ,
258+ color : '#666666' ,
259+ textTransform : 'uppercase' ,
260+ letterSpacing : '0.08em' ,
261+ fontWeight : 500 ,
262+ } }
263+ >
264+ { post . readingTime } min read
265+ </ span >
266+ ) }
267+ </ div >
175268 < div
176269 style = { {
177270 fontSize : getTitleFontSize ( post . title ) ,
@@ -199,14 +292,16 @@ export async function GET(request: NextRequest) {
199292 : post . description }
200293 </ div >
201294 </ div >
295+
202296 < div
203297 style = { {
204298 display : 'flex' ,
205299 justifyContent : 'space-between' ,
206300 alignItems : 'center' ,
207301 zIndex : 1 ,
208302 borderTop : '1px solid #2A2A2A' ,
209- paddingTop : 24 ,
303+ paddingTop : 30 ,
304+ marginBottom : 30 ,
210305 } }
211306 >
212307 < div style = { { display : 'flex' , alignItems : 'center' , gap : 16 } } >
@@ -240,17 +335,11 @@ export async function GET(request: NextRequest) {
240335 height : 630 ,
241336 fonts : [
242337 {
243- name : 'Season Sans ' ,
244- data : fontMedium ,
338+ name : 'Inter ' ,
339+ data : fontData ,
245340 style : 'normal' as const ,
246341 weight : 500 as const ,
247342 } ,
248- {
249- name : 'Season Sans' ,
250- data : fontBold ,
251- style : 'normal' as const ,
252- weight : 700 as const ,
253- } ,
254343 ] ,
255344 }
256345 )
0 commit comments