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 " ;
33import { getBlogBySlug , getBlogSlugs } from "@/lib/blog" ;
4+ import { highlightCode } from "@/lib/code-highlight" ;
45import { formatDate , slugify } from "@/lib/utils" ;
5- import { blogDataset , blogProjectId } from "@/sanity/blog-env" ;
66import { urlForBlogImage } from "@/sanity/blog-image" ;
77import { IBlogArticle , IBlogHeading } from "@/types/blog" ;
8+ import { PortableText , type PortableTextComponents } from "@portabletext/react" ;
9+ import { ArrowLeft , Link2 } from "lucide-react" ;
810import Image from "next/image" ;
911import Link from "next/link" ;
1012import { 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