@@ -62,12 +62,22 @@ const loadBlogIndex = createServerFn({ method: "GET" }).handler(async () => {
6262 ?. map ( ( authorId ) => authorsMap [ authorId ] )
6363 . filter ( Boolean ) as Author [ ] | undefined ;
6464
65+ // Extract og:image from frontmatter - use relative paths for local assets
66+ const ogImageRaw =
67+ typeof parsed . frontmatter ?. [ "og:image" ] === "string"
68+ ? parsed . frontmatter [ "og:image" ]
69+ : undefined ;
70+ const ogImage = ogImageRaw
71+ ? resolveLocalBlogAsset ( ogImageRaw , item . slug )
72+ : undefined ;
73+
6574 return {
6675 slug : item . slug ,
6776 title,
6877 description,
6978 date : item . date ,
7079 authors,
80+ ogImage,
7181 } ;
7282 } ) ,
7383 ) ;
@@ -143,46 +153,85 @@ function BlogIndexPage() {
143153 < main className = "bg-white text-slate-900" >
144154 < div className = "mx-auto max-w-3xl px-6 py-16" >
145155 { /* Header */ }
146- < h1 className = "text-4xl font-bold tracking-tight text-slate-900 mb-12 " >
156+ < h1 className = "text-4xl font-bold tracking-tight text-slate-900 mb-6 " >
147157 Blog
148158 </ h1 >
149159
160+ { /* Subscribe CTA */ }
161+ < form
162+ action = "https://buttondown.com/api/emails/embed-subscribe/inlangs-blog"
163+ method = "post"
164+ target = "_blank"
165+ className = "mb-12"
166+ >
167+ < p className = "text-sm text-slate-500 mb-3" >
168+ Get notified about new blog posts
169+ </ p >
170+ < div className = "flex gap-2" >
171+ < input
172+ type = "email"
173+ name = "email"
174+ placeholder = "your@email.com"
175+ required
176+ className = "flex-1 px-4 py-2 text-sm border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-slate-900 focus:border-transparent"
177+ />
178+ < button
179+ type = "submit"
180+ className = "px-4 py-2 text-sm font-medium text-slate-900 border border-slate-300 rounded-md hover:bg-slate-50 transition-colors"
181+ >
182+ Subscribe
183+ </ button >
184+ </ div >
185+ </ form >
186+
150187 { /* Blog post list */ }
151- < div className = "flex flex-col gap-1 " >
188+ < div className = "flex flex-col gap-6 " >
152189 { posts . map ( ( post ) => (
153190 < Link
154191 key = { post . slug }
155192 to = "/blog/$slug"
156193 params = { { slug : post . slug } }
157194 className = "group block rounded-xl p-6 -mx-6 hover:bg-slate-50 transition-colors"
158195 >
159- < article >
160- < h2 className = "text-xl font-semibold text-slate-900 group-hover:text-slate-700 transition-colors" >
161- { post . title ?? post . slug }
162- </ h2 >
163- < div className = "mt-3 flex items-center gap-2 text-sm text-slate-500" >
164- { post . authors && post . authors . length > 0 && (
165- < >
166- { post . authors . map ( ( author , index ) => (
167- < div key = { index } className = "flex items-center gap-2" >
168- { author . avatar ? (
169- < img
170- src = { author . avatar }
171- alt = { author . name }
172- className = "w-5 h-5 rounded-full object-cover"
173- />
174- ) : (
175- < div className = "w-5 h-5 rounded-full bg-slate-300 flex items-center justify-center text-xs text-slate-600 font-medium" >
176- { author . name . charAt ( 0 ) }
177- </ div >
178- ) }
179- < span > { author . name } </ span >
180- </ div >
181- ) ) }
182- { post . date && < span className = "text-slate-300" > ·</ span > }
183- </ >
184- ) }
185- { post . date && < time > { formatDate ( post . date ) } </ time > }
196+ < article className = "flex gap-6" >
197+ { /* OG Image */ }
198+ { post . ogImage && (
199+ < div className = "flex-shrink-0 w-40 h-24 rounded-lg overflow-hidden bg-slate-100" >
200+ < img
201+ src = { post . ogImage }
202+ alt = ""
203+ className = "w-full h-full object-cover"
204+ />
205+ </ div >
206+ ) }
207+ < div className = "flex-1 min-w-0" >
208+ < h2 className = "text-xl font-semibold text-slate-900 group-hover:text-slate-700 transition-colors" >
209+ { post . title ?? post . slug }
210+ </ h2 >
211+ < div className = "mt-3 flex items-center gap-2 text-sm text-slate-500" >
212+ { post . authors && post . authors . length > 0 && (
213+ < >
214+ { post . authors . map ( ( author , index ) => (
215+ < div key = { index } className = "flex items-center gap-2" >
216+ { author . avatar ? (
217+ < img
218+ src = { author . avatar }
219+ alt = { author . name }
220+ className = "w-5 h-5 rounded-full object-cover"
221+ />
222+ ) : (
223+ < div className = "w-5 h-5 rounded-full bg-slate-300 flex items-center justify-center text-xs text-slate-600 font-medium" >
224+ { author . name . charAt ( 0 ) }
225+ </ div >
226+ ) }
227+ < span > { author . name } </ span >
228+ </ div >
229+ ) ) }
230+ { post . date && < span className = "text-slate-300" > ·</ span > }
231+ </ >
232+ ) }
233+ { post . date && < time > { formatDate ( post . date ) } </ time > }
234+ </ div >
186235 </ div >
187236 </ article >
188237 </ Link >
@@ -222,3 +271,11 @@ function getBlogMarkdown(relativePath: string): Promise<string> {
222271 }
223272 return loader ( ) ;
224273}
274+
275+ function resolveLocalBlogAsset ( value : string , slug : string ) : string {
276+ // If it's already an absolute URL, return as-is
277+ if ( / ^ [ a - z ] [ a - z 0 - 9 + . - ] * : / . test ( value ) ) return value ;
278+ // Convert relative paths (like ./assets/image.png) to /blog/slug/assets/image.png
279+ const normalized = value . replace ( / ^ \. \/ / , "" ) ;
280+ return `/blog/${ slug } /${ normalized } ` ;
281+ }
0 commit comments