Skip to content

Commit 4175d31

Browse files
add newsletter sign up
1 parent 52f92dd commit 4175d31

File tree

2 files changed

+114
-30
lines changed

2 files changed

+114
-30
lines changed

packages/website-v2/src/routes/blog/$slug.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,9 +475,36 @@ function BlogPostPage() {
475475
dangerouslySetInnerHTML={{ __html: html }}
476476
/>
477477

478+
{/* Subscribe CTA */}
479+
<form
480+
action="https://buttondown.com/api/emails/embed-subscribe/inlangs-blog"
481+
method="post"
482+
target="_blank"
483+
className="mt-16 border-t border-slate-200 pt-8"
484+
>
485+
<p className="text-sm text-slate-500 mb-3">
486+
Get notified about new blog posts
487+
</p>
488+
<div className="flex gap-2">
489+
<input
490+
type="email"
491+
name="email"
492+
placeholder="your@email.com"
493+
required
494+
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"
495+
/>
496+
<button
497+
type="submit"
498+
className="px-4 py-2 text-sm font-medium text-slate-900 border border-slate-300 rounded-md hover:bg-slate-50 transition-colors"
499+
>
500+
Subscribe
501+
</button>
502+
</div>
503+
</form>
504+
478505
{/* Previous / Next navigation */}
479506
{(prevPost || nextPost) && (
480-
<nav className="mt-16 grid grid-cols-2 gap-4 border-t border-slate-200 pt-8">
507+
<nav className="mt-8 grid grid-cols-2 gap-4">
481508
{/* Previous post (newer) */}
482509
<div>
483510
{prevPost && (

packages/website-v2/src/routes/blog/index.tsx

Lines changed: 86 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -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-z0-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

Comments
 (0)