Skip to content

Commit 2d0f73f

Browse files
chore(website): redesign blog post page (#5184)
# Description Please include a summary of the changes and the related issue. Please also include relevant motivation and context. ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update ## How Has This Been Tested? Please describe the tests that you ran to verify your changes. ## Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes
1 parent 6b089f6 commit 2d0f73f

6 files changed

Lines changed: 384 additions & 494 deletions

File tree

website/astro.config.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ export default defineConfig({
5353
'/solutions/per-tenant-db': '/',
5454
'/solutions/user-session-store': '/',
5555
'/solutions/workflows': '/',
56+
// Changelog list view merged into the blog index
57+
'/changelog': '/blog',
5658
},
5759
prefetch: {
5860
prefetchAll: true,
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
---
2+
import { getCollection, render } from 'astro:content';
3+
import { getPostImage } from '@/lib/postImage';
4+
import { ArticleSocials } from '@/components/ArticleSocials';
5+
import { Prose } from '@/components/Prose';
6+
import { formatTimestamp } from '@/lib/formatDate';
7+
import { CATEGORIES } from '@/lib/article';
8+
import { Icon, faArrowLeft, faArrowRight } from '@rivet-gg/icons';
9+
import * as mdxComponents from '@/components/mdx';
10+
11+
interface Props {
12+
// biome-ignore lint/suspicious/noExplicitAny: content collection entry
13+
entry: any;
14+
image?: { src: string; width: number; height: number } | null;
15+
section: 'blog' | 'changelog';
16+
}
17+
18+
const { entry, image, section } = Astro.props;
19+
const { Content } = await render(entry);
20+
21+
const { title, description } = entry.data as unknown as { title: string; description: string };
22+
23+
// "Read next" pulls from the same section the reader is currently in.
24+
const readNextBase = section === 'changelog' ? '/changelog/' : '/blog/';
25+
const allPosts = await getCollection('posts');
26+
const otherArticles = allPosts
27+
.filter(
28+
(p) =>
29+
p.id !== entry.id &&
30+
!p.data.unpublished &&
31+
(section === 'changelog' ? p.data.category === 'changelog' : p.data.category !== 'changelog'),
32+
)
33+
.sort((a, b) => b.data.published.getTime() - a.data.published.getTime())
34+
.slice(0, 3)
35+
.map((post) => {
36+
const data = post.data as unknown as { title: string };
37+
return {
38+
slug: post.id.replace(/\/page$/, ''),
39+
title: data.title,
40+
category: { ...CATEGORIES[post.data.category], id: post.data.category },
41+
published: post.data.published,
42+
image: getPostImage(post),
43+
};
44+
});
45+
---
46+
47+
<div class="blog-article relative w-full" style="--header-height: 5rem;">
48+
<div class="mx-auto w-full max-w-[62rem] px-6 pb-24 pt-32 md:pt-40">
49+
<article class="mx-auto w-full max-w-[44rem]">
50+
<!-- Back link -->
51+
<a
52+
href="/blog/"
53+
class="group flex items-center gap-2 text-sm text-zinc-500 transition-colors hover:text-white"
54+
>
55+
<Icon icon={faArrowLeft} className="h-3 w-auto transition-transform group-hover:-translate-x-0.5" />
56+
Blog
57+
</a>
58+
59+
<!-- Header -->
60+
<header class="mb-12 mt-8">
61+
<time datetime={entry.data.published.toISOString()} class="text-sm font-medium text-zinc-500">
62+
{formatTimestamp(entry.data.published)}
63+
</time>
64+
<h1 class="mt-2 text-4xl font-normal leading-[1.1] tracking-tight text-white [text-wrap:balance] md:text-[3.25rem]">
65+
{title}
66+
</h1>
67+
{description && (
68+
<p class="mt-5 text-lg font-light leading-7 text-zinc-400 [text-wrap:pretty] md:text-xl">
69+
{description}
70+
</p>
71+
)}
72+
</header>
73+
74+
{image && (
75+
<img
76+
src={image.src}
77+
alt={title}
78+
width={image.width}
79+
height={image.height}
80+
class="mb-12 aspect-[2/1] w-full rounded-2xl border border-white/10 object-cover"
81+
loading="eager"
82+
decoding="async"
83+
/>
84+
)}
85+
86+
<Prose as="div" className="blog-prose w-full max-w-none">
87+
<Content components={mdxComponents} />
88+
</Prose>
89+
90+
<div class="mt-16 border-t border-white/10 pt-8">
91+
<ArticleSocials title={title} client:load />
92+
</div>
93+
</article>
94+
95+
<!-- Read next -->
96+
{otherArticles.length > 0 && (
97+
<div class="mx-auto mt-24 w-full max-w-[62rem] border-t border-white/10 pt-10">
98+
<h2 class="text-sm font-medium uppercase tracking-[0.12em] text-zinc-500">Read next</h2>
99+
<div class="mt-6 grid grid-cols-1 gap-6 md:grid-cols-3">
100+
{otherArticles.map((article) => (
101+
<a href={`${readNextBase}${article.slug}/`} class="group flex flex-col">
102+
{article.image && (
103+
<img
104+
src={article.image.src}
105+
alt={article.title}
106+
width={600}
107+
height={300}
108+
class="aspect-[2/1] w-full rounded-xl border border-white/10 object-cover transition-colors group-hover:border-white/25"
109+
loading="lazy"
110+
decoding="async"
111+
/>
112+
)}
113+
<div class="mt-3 flex items-center gap-x-3 text-xs">
114+
<span class="text-accent font-semibold uppercase tracking-[0.1em]">{article.category.name}</span>
115+
<time datetime={article.published.toISOString()} class="text-zinc-500">
116+
{formatTimestamp(article.published)}
117+
</time>
118+
</div>
119+
<h3 class="mt-2 text-base font-normal leading-snug text-white transition-colors group-hover:text-accent">
120+
{article.title}
121+
</h3>
122+
<span class="mt-2 inline-flex items-center gap-1.5 text-xs font-medium text-zinc-500 transition-colors group-hover:text-white">
123+
Read article
124+
<Icon icon={faArrowRight} className="h-2.5 w-auto transition-transform group-hover:translate-x-0.5" />
125+
</span>
126+
</a>
127+
))}
128+
</div>
129+
</div>
130+
)}
131+
</div>
132+
</div>
133+
134+
<style is:global>
135+
/* Editorial typography scoped to blog + changelog articles only, so docs Prose stays untouched. */
136+
.blog-article .blog-prose {
137+
font-size: 1.0625rem;
138+
line-height: 1.8;
139+
color: hsl(var(--muted-foreground));
140+
}
141+
142+
.blog-article .blog-prose p,
143+
.blog-article .blog-prose ul,
144+
.blog-article .blog-prose ol {
145+
margin-top: 1.4em;
146+
margin-bottom: 1.4em;
147+
}
148+
149+
.blog-article .blog-prose > :first-child {
150+
margin-top: 0;
151+
}
152+
153+
.blog-article .blog-prose h2 {
154+
font-size: 1.75rem;
155+
line-height: 1.2;
156+
letter-spacing: -0.02em;
157+
margin-top: 2.75rem;
158+
margin-bottom: 1rem;
159+
color: #fff;
160+
}
161+
162+
.blog-article .blog-prose h3 {
163+
font-size: 1.3rem;
164+
line-height: 1.3;
165+
letter-spacing: -0.015em;
166+
margin-top: 2.25rem;
167+
margin-bottom: 0.75rem;
168+
color: #fff;
169+
}
170+
171+
.blog-article .blog-prose h4 {
172+
font-size: 1.075rem;
173+
margin-top: 2rem;
174+
margin-bottom: 0.5rem;
175+
color: #fff;
176+
}
177+
178+
.blog-article .blog-prose a {
179+
color: #ff6a33;
180+
text-decoration: underline;
181+
text-decoration-color: rgb(255 69 0 / 0.35);
182+
text-underline-offset: 0.2em;
183+
text-decoration-thickness: 1px;
184+
font-weight: 500;
185+
transition: text-decoration-color 0.15s ease, color 0.15s ease;
186+
}
187+
188+
.blog-article .blog-prose a:hover {
189+
color: #ff8a5c;
190+
text-decoration-color: rgb(255 69 0 / 0.9);
191+
}
192+
193+
.blog-article .blog-prose strong {
194+
color: #fff;
195+
font-weight: 600;
196+
}
197+
198+
.blog-article .blog-prose :not(pre) > code {
199+
background: rgb(255 255 255 / 0.06);
200+
border: 1px solid rgb(255 255 255 / 0.08);
201+
border-radius: 0.375rem;
202+
padding: 0.1em 0.4em;
203+
font-size: 0.875em;
204+
color: #e6e6e6;
205+
}
206+
207+
.blog-article .blog-prose pre {
208+
border-radius: 0.75rem;
209+
border: 1px solid rgb(255 255 255 / 0.1);
210+
background: rgb(255 255 255 / 0.025);
211+
padding: 1.25rem 1.4rem;
212+
font-size: 0.9rem;
213+
line-height: 1.7;
214+
margin-top: 1.75rem;
215+
margin-bottom: 1.75rem;
216+
}
217+
218+
.blog-article .blog-prose blockquote {
219+
border-left: 2px solid #ff4500;
220+
padding-left: 1.25rem;
221+
font-style: normal;
222+
color: hsl(var(--muted-foreground));
223+
}
224+
225+
.blog-article .blog-prose img,
226+
.blog-article .blog-prose video {
227+
border-radius: 0.75rem;
228+
border: 1px solid rgb(255 255 255 / 0.1);
229+
margin-top: 2rem;
230+
margin-bottom: 2rem;
231+
}
232+
233+
.blog-article .blog-prose hr {
234+
border-color: rgb(255 255 255 / 0.1);
235+
margin-top: 2.75rem;
236+
margin-bottom: 2.75rem;
237+
}
238+
239+
.blog-article .blog-prose li {
240+
margin-top: 0.4em;
241+
margin-bottom: 0.4em;
242+
}
243+
244+
.blog-article .blog-prose h2 a,
245+
.blog-article .blog-prose h3 a,
246+
.blog-article .blog-prose h4 a {
247+
color: inherit;
248+
text-decoration: none;
249+
}
250+
</style>

0 commit comments

Comments
 (0)