Skip to content

Commit 5268ad3

Browse files
author
marcus
committed
feat(blog): redesign article pages and improve reading experience
1 parent 1046fba commit 5268ad3

6 files changed

Lines changed: 561 additions & 141 deletions

File tree

app/(app)/blog/[slug]/page.tsx

Lines changed: 107 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { PortableText, type PortableTextComponents } from "@portabletext/react";
2-
import { Button } from "@/components/ui/button";
1+
import ReadingProgress from "@/app/components/blog/ReadingProgress";
2+
import CodeBlock from "@/app/components/blog/CodeBlock";
33
import { getBlogBySlug, getBlogSlugs } from "@/lib/blog";
4+
import { highlightCode } from "@/lib/code-highlight";
45
import { formatDate, slugify } from "@/lib/utils";
5-
import { blogDataset, blogProjectId } from "@/sanity/blog-env";
66
import { urlForBlogImage } from "@/sanity/blog-image";
77
import { IBlogArticle, IBlogHeading } from "@/types/blog";
8+
import { PortableText, type PortableTextComponents } from "@portabletext/react";
9+
import { ArrowLeft, Link2 } from "lucide-react";
810
import Image from "next/image";
911
import Link from "next/link";
1012
import { notFound } from "next/navigation";
@@ -29,27 +31,32 @@ const portableTextComponents: PortableTextComponents = {
2931
h2: ({ children, value }) => (
3032
<h2
3133
id={slugify(getHeadingText(value as IBlogHeading))}
32-
className="mt-12 scroll-mt-28 font-incognito text-3xl font-semibold"
34+
className="mt-14 scroll-mt-28 font-incognito text-[2rem] leading-[0.98] tracking-[-0.03em] md:text-[2.35rem]"
3335
>
3436
{children}
3537
</h2>
3638
),
3739
h3: ({ children, value }) => (
3840
<h3
3941
id={slugify(getHeadingText(value as IBlogHeading))}
40-
className="mt-10 scroll-mt-28 font-incognito text-2xl font-semibold"
42+
className="mt-12 scroll-mt-28 font-incognito text-[1.55rem] leading-[1] tracking-[-0.02em] md:text-[1.85rem]"
4143
>
4244
{children}
4345
</h3>
4446
),
4547
h4: ({ children, value }) => (
4648
<h4
4749
id={slugify(getHeadingText(value as IBlogHeading))}
48-
className="mt-8 scroll-mt-28 font-incognito text-xl font-semibold"
50+
className="mt-10 scroll-mt-28 font-incognito text-[1.2rem] leading-[1.05] tracking-[-0.01em] md:text-[1.35rem]"
4951
>
5052
{children}
5153
</h4>
5254
),
55+
blockquote: ({ children }) => (
56+
<blockquote className="my-8 border-l-2 border-primary/40 pl-5 font-incognito text-[1.2rem] leading-8 text-foreground/88 md:text-[1.35rem]">
57+
{children}
58+
</blockquote>
59+
),
5360
},
5461
types: {
5562
image: ({ value }) => {
@@ -58,7 +65,7 @@ const portableTextComponents: PortableTextComponents = {
5865
}
5966

6067
return (
61-
<div className="my-8 overflow-hidden rounded-2xl border border-border/70">
68+
<div className="my-10 overflow-hidden rounded-[1.8rem] border border-border/70">
6269
<Image
6370
src={urlForBlogImage(value)}
6471
alt={value.alt || "Blog image"}
@@ -69,11 +76,12 @@ const portableTextComponents: PortableTextComponents = {
6976
</div>
7077
);
7178
},
72-
code: ({ value }) => (
73-
<pre className="my-6 overflow-x-auto rounded-2xl border border-border/70 bg-muted/60 p-4 text-sm leading-6">
74-
<code>{value.code}</code>
75-
</pre>
76-
),
79+
code: ({ value }) => {
80+
const code = value?.code || "";
81+
const { highlighted, language } = highlightCode(code, value?.language);
82+
83+
return <CodeBlock code={code} language={language} highlighted={highlighted} />;
84+
},
7785
table: ({ value }) => {
7886
const rows = value?.rows || [];
7987

@@ -82,7 +90,7 @@ const portableTextComponents: PortableTextComponents = {
8290
}
8391

8492
return (
85-
<div className="my-8 overflow-x-auto rounded-2xl border border-border/70">
93+
<div className="my-10 overflow-x-auto rounded-[1.6rem] border border-border/70">
8694
<table className="min-w-full border-collapse text-sm">
8795
<tbody>
8896
{rows.map((row: { _key?: string; cells?: string[] }, rowIndex: number) => (
@@ -110,7 +118,7 @@ const portableTextComponents: PortableTextComponents = {
110118
href={href}
111119
target={isExternal ? "_blank" : undefined}
112120
rel={isExternal ? "noreferrer noopener" : undefined}
113-
className="text-primary underline decoration-primary/40 underline-offset-4 transition-colors hover:decoration-primary"
121+
className="text-primary underline decoration-primary/35 underline-offset-4 transition-colors hover:decoration-primary"
114122
>
115123
{children}
116124
</a>
@@ -137,65 +145,100 @@ export default async function BlogArticlePage({ params }: { params: Promise<{ sl
137145
const coverImageUrl = article.mainImage ? urlForBlogImage(article.mainImage) : null;
138146

139147
return (
140-
<section className="mx-auto flex max-w-6xl flex-col gap-10 px-6 pb-24 pt-28 md:px-16">
141-
<div className="flex flex-col gap-6 border-b border-border/70 pb-10">
142-
<Link
143-
href="/blog"
144-
className="text-sm uppercase tracking-[0.2em] text-muted-foreground transition-colors hover:text-foreground"
145-
>
146-
Back to blog
147-
</Link>
148-
149-
<div className="flex max-w-4xl flex-col gap-5">
150-
<h1 className="font-incognito text-4xl font-semibold leading-tight md:text-6xl">
151-
{article.title}
152-
</h1>
153-
<p className="text-base leading-7 text-muted-foreground md:text-lg">
154-
{article.smallDesc}
155-
</p>
156-
</div>
148+
<section className="mx-auto flex max-w-7xl flex-col gap-10 px-6 pb-24 pt-28 md:px-16">
149+
<ReadingProgress />
150+
<div className="border-b border-border/70 pb-10">
151+
<div className="grid gap-8 xl:grid-cols-[minmax(0,0.78fr)_minmax(0,1.22fr)] xl:items-end">
152+
<div className="space-y-4">
153+
<Link
154+
href="/blog"
155+
className="inline-flex items-center gap-2 text-[0.72rem] uppercase tracking-[0.22em] text-muted-foreground transition-colors hover:text-foreground"
156+
>
157+
<ArrowLeft className="h-4 w-4" />
158+
Back to blog
159+
</Link>
157160

158-
<div className="flex flex-wrap items-center gap-3 text-sm text-muted-foreground">
159-
<span>{formatDate(article.date)}</span>
160-
<span className="h-1 w-1 rounded-full bg-primary/70" />
161-
<span>{getAuthorName(article.author)}</span>
162-
{article.categories?.length ? (
163-
<>
164-
<span className="h-1 w-1 rounded-full bg-primary/70" />
165-
<span>{article.categories.map((category) => category.title).join(", ")}</span>
166-
</>
167-
) : null}
161+
<div className="flex flex-wrap gap-2.5">
162+
{article.categories?.length ? (
163+
article.categories.map((category) => (
164+
<span
165+
key={`${article.slug}-${category.title}`}
166+
className="rounded-full border border-border/70 px-3 py-1 text-[0.72rem] font-medium uppercase tracking-[0.18em] text-foreground/88"
167+
style={{
168+
borderColor: "color-mix(in oklch, var(--border) 58%, var(--primary) 42%)",
169+
background:
170+
"linear-gradient(135deg, color-mix(in oklch, var(--background) 84%, var(--primary) 16%), color-mix(in oklch, var(--background) 94%, var(--primary) 6%))",
171+
}}
172+
>
173+
{category.title}
174+
</span>
175+
))
176+
) : (
177+
<span
178+
className="rounded-full border border-border/70 px-3 py-1 text-[0.72rem] font-medium uppercase tracking-[0.18em] text-foreground/88"
179+
style={{
180+
borderColor: "color-mix(in oklch, var(--border) 58%, var(--secondary) 42%)",
181+
background:
182+
"linear-gradient(135deg, color-mix(in oklch, var(--background) 84%, var(--secondary) 16%), color-mix(in oklch, var(--background) 94%, var(--secondary) 6%))",
183+
}}
184+
>
185+
Article
186+
</span>
187+
)}
188+
</div>
189+
190+
<h1 className="max-w-4xl font-incognito text-[clamp(2.8rem,6vw,5.8rem)] leading-[0.92] tracking-[-0.055em]">
191+
{article.title}
192+
</h1>
193+
</div>
194+
195+
<div className="flex flex-col gap-5 xl:items-end">
196+
<p className="max-w-2xl text-base leading-7 text-muted-foreground md:text-lg xl:text-right">
197+
{article.smallDesc}
198+
</p>
199+
200+
<div className="flex flex-wrap gap-4 text-sm text-muted-foreground xl:justify-end">
201+
<span>{formatDate(article.date)}</span>
202+
<span>{getAuthorName(article.author)}</span>
203+
</div>
204+
</div>
168205
</div>
169206
</div>
170207

171-
<div className="grid gap-10 lg:grid-cols-[minmax(0,1fr)_240px]">
208+
<div className="grid gap-10 xl:grid-cols-[minmax(0,1fr)_17rem]">
172209
<article className="min-w-0">
173210
{coverImageUrl ? (
174-
<div className="relative mb-10 aspect-[16/9] overflow-hidden rounded-[2rem] border border-border/70">
175-
<Image
176-
src={coverImageUrl}
177-
alt={article.title}
178-
fill
179-
priority
180-
className="object-cover"
181-
sizes="(max-width: 1024px) 100vw, 70vw"
182-
/>
211+
<div className="relative mb-10 overflow-hidden rounded-[2.2rem] border border-border/70">
212+
<div className="relative aspect-[16/9]">
213+
<Image
214+
src={coverImageUrl}
215+
alt={article.title}
216+
fill
217+
priority
218+
className="object-cover"
219+
sizes="(max-width: 1280px) 100vw, 72vw"
220+
/>
221+
</div>
183222
</div>
184223
) : null}
185224

186-
<div className="prose prose-neutral max-w-none dark:prose-invert prose-headings:font-incognito prose-a:text-primary prose-pre:bg-transparent">
187-
<PortableText
188-
value={article.body}
189-
components={portableTextComponents}
190-
onMissingComponent={false}
191-
/>
225+
<div className="gap-8">
226+
<div className="min-w-0">
227+
<div className="prose prose-neutral max-w-none dark:prose-invert prose-headings:font-incognito prose-a:text-primary prose-pre:bg-transparent prose-p:text-base prose-p:leading-8 prose-p:text-muted-foreground prose-li:text-base prose-li:leading-8 prose-li:text-muted-foreground prose-strong:text-foreground prose-hr:border-border/60">
228+
<PortableText
229+
value={article.body}
230+
components={portableTextComponents}
231+
onMissingComponent={false}
232+
/>
233+
</div>
234+
</div>
192235
</div>
193236
</article>
194237

195-
<aside className="space-y-4 lg:sticky lg:top-28 lg:self-start">
196-
<div className="rounded-2xl border border-border/70 bg-background/80 p-5 backdrop-blur-sm">
197-
<p className="text-sm uppercase tracking-[0.2em] text-muted-foreground">Contents</p>
198-
<div className="mt-4 flex flex-col gap-3">
238+
<aside className="space-y-5 xl:sticky xl:top-28 xl:self-start">
239+
<div className="border-t border-border/65 pt-5">
240+
<p className="text-[0.72rem] uppercase tracking-[0.24em] text-primary">Contents</p>
241+
<div className="mt-5 flex flex-col gap-3">
199242
{headings.length ? (
200243
headings.map((heading) => {
201244
const text = getHeadingText(heading);
@@ -204,9 +247,10 @@ export default async function BlogArticlePage({ params }: { params: Promise<{ sl
204247
<a
205248
key={heading._key}
206249
href={`#${slugify(text)}`}
207-
className="text-sm leading-6 text-muted-foreground transition-colors hover:text-foreground"
250+
className="inline-flex items-start gap-2 text-sm leading-6 text-muted-foreground transition-colors hover:text-foreground"
208251
>
209-
{text}
252+
<Link2 className="mt-1 h-3.5 w-3.5 shrink-0 text-primary/70" />
253+
<span>{text}</span>
210254
</a>
211255
);
212256
})
@@ -217,17 +261,6 @@ export default async function BlogArticlePage({ params }: { params: Promise<{ sl
217261
)}
218262
</div>
219263
</div>
220-
221-
<div className="rounded-2xl border border-border/70 bg-muted/30 p-5">
222-
<p className="text-sm leading-6 text-muted-foreground">
223-
Source: Sanity project{" "}
224-
<span className="font-mono text-foreground">{blogProjectId}</span> in dataset{" "}
225-
<span className="font-mono text-foreground">{blogDataset}</span>.
226-
</p>
227-
<Button asChild variant="outline" className="mt-4 w-full">
228-
<Link href="/blog">Browse more posts</Link>
229-
</Button>
230-
</div>
231264
</aside>
232265
</div>
233266
</section>

0 commit comments

Comments
 (0)