@@ -4,7 +4,7 @@ import Link from 'next/link'
44import { Avatar , AvatarFallback , AvatarImage } from '@/components/emcn'
55import { FAQ } from '@/lib/blog/faq'
66import { getAllPostMeta , getPostBySlug , getRelatedPosts } from '@/lib/blog/registry'
7- import { buildArticleJsonLd , buildBreadcrumbJsonLd , buildPostMetadata } from '@/lib/blog/seo'
7+ import { buildPostGraphJsonLd , buildPostMetadata } from '@/lib/blog/seo'
88import { getBaseUrl } from '@/lib/core/utils/urls'
99import { BackLink } from '@/app/(landing)/blog/[slug]/back-link'
1010import { ShareButton } from '@/app/(landing)/blog/[slug]/share-button'
@@ -30,27 +30,27 @@ export default async function Page({ params }: { params: Promise<{ slug: string
3030 const { slug } = await params
3131 const post = await getPostBySlug ( slug )
3232 const Article = post . Content
33- const jsonLd = buildArticleJsonLd ( post )
34- const breadcrumbLd = buildBreadcrumbJsonLd ( post )
33+ const graphJsonLd = buildPostGraphJsonLd ( post )
3534 const related = await getRelatedPosts ( slug , 3 )
3635
3736 return (
38- < article className = 'w-full' itemScope itemType = 'https://schema.org/BlogPosting' >
37+ < article
38+ className = 'w-full bg-[var(--landing-bg)]'
39+ itemScope
40+ itemType = 'https://schema.org/TechArticle'
41+ >
3942 < script
4043 type = 'application/ld+json'
41- dangerouslySetInnerHTML = { { __html : JSON . stringify ( jsonLd ) } }
44+ dangerouslySetInnerHTML = { { __html : JSON . stringify ( graphJsonLd ) } }
4245 />
43- < script
44- type = 'application/ld+json'
45- dangerouslySetInnerHTML = { { __html : JSON . stringify ( breadcrumbLd ) } }
46- />
47- < header className = 'mx-auto max-w-[1450px] px-6 pt-8 sm:px-8 sm:pt-12 md:px-12 md:pt-16' >
46+ < header className = 'px-5 pt-[60px] lg:px-16 lg:pt-[100px]' >
4847 < div className = 'mb-6' >
4948 < BackLink />
5049 </ div >
50+
5151 < div className = 'flex flex-col gap-8 md:flex-row md:gap-12' >
5252 < div className = 'w-full flex-shrink-0 md:w-[450px]' >
53- < div className = 'relative w-full overflow-hidden rounded-lg ' >
53+ < div className = 'relative w-full overflow-hidden rounded-[5px] ' >
5454 < Image
5555 src = { post . ogImage }
5656 alt = { post . title }
@@ -65,18 +65,35 @@ export default async function Page({ params }: { params: Promise<{ slug: string
6565 </ div >
6666 </ div >
6767 < div className = 'flex flex-1 flex-col justify-between' >
68- < h1
69- className = 'text-balance font-[500] text-[36px] text-[var(--landing-text)] leading-tight tracking-tight sm:text-[48px] md:text-[56px] lg:text-[64px]'
70- itemProp = 'headline'
71- >
72- { post . title }
73- </ h1 >
74- < div className = 'mt-4 flex items-center justify-between' >
68+ < div >
69+ < h1
70+ className = 'text-balance font-[430] font-season text-[28px] text-white leading-[110%] tracking-[-0.02em] sm:text-[36px] md:text-[44px] lg:text-[52px]'
71+ itemProp = 'headline'
72+ >
73+ { post . title }
74+ </ h1 >
75+ < p className = 'mt-4 font-[430] font-season text-[var(--landing-text-body)] text-base leading-[150%] tracking-[0.02em] sm:text-lg' >
76+ { post . description }
77+ </ p >
78+ </ div >
79+ < div className = 'mt-6 flex items-center gap-6' >
80+ < time
81+ className = 'font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'
82+ dateTime = { post . date }
83+ itemProp = 'datePublished'
84+ >
85+ { new Date ( post . date ) . toLocaleDateString ( 'en-US' , {
86+ month : 'short' ,
87+ day : 'numeric' ,
88+ year : 'numeric' ,
89+ } ) }
90+ </ time >
91+ < meta itemProp = 'dateModified' content = { post . updated ?? post . date } />
7592 < div className = 'flex items-center gap-3' >
7693 { ( post . authors || [ post . author ] ) . map ( ( a , idx ) => (
7794 < div key = { idx } className = 'flex items-center gap-2' >
7895 { a ?. avatarUrl ? (
79- < Avatar className = 'size-6 ' >
96+ < Avatar className = 'size-5 ' >
8097 < AvatarImage src = { a . avatarUrl } alt = { a . name } />
8198 < AvatarFallback > { a . name . slice ( 0 , 2 ) } </ AvatarFallback >
8299 </ Avatar >
@@ -85,7 +102,7 @@ export default async function Page({ params }: { params: Promise<{ slug: string
85102 href = { a ?. url || '#' }
86103 target = '_blank'
87104 rel = 'noopener noreferrer author'
88- className = 'text-[var(--landing-text-muted)] text-sm leading-[1.5 ] hover:text-[var(--landing-text)] sm:text-md '
105+ className = 'font-martian-mono text-[var(--landing-text-muted)] text-xs uppercase tracking-[0.1em ] hover:text-white '
89106 itemProp = 'author'
90107 itemScope
91108 itemType = 'https://schema.org/Person'
@@ -95,78 +112,72 @@ export default async function Page({ params }: { params: Promise<{ slug: string
95112 </ div >
96113 ) ) }
97114 </ div >
98- < ShareButton url = { `${ getBaseUrl ( ) } /blog/${ slug } ` } title = { post . title } />
115+ < div className = 'ml-auto' >
116+ < ShareButton url = { `${ getBaseUrl ( ) } /blog/${ slug } ` } title = { post . title } />
117+ </ div >
99118 </ div >
100119 </ div >
101120 </ div >
102- < hr className = 'mt-8 border-[var(--landing-bg-elevated)] border-t sm:mt-12' />
103- < div className = 'flex flex-col gap-6 py-8 sm:flex-row sm:items-start sm:justify-between sm:gap-8 sm:py-10' >
104- < div className = 'flex flex-shrink-0 items-center gap-4' >
105- < time
106- className = 'block text-[var(--landing-text-muted)] text-sm leading-[1.5] sm:text-md'
107- dateTime = { post . date }
108- itemProp = 'datePublished'
109- >
110- { new Date ( post . date ) . toLocaleDateString ( 'en-US' , {
111- month : 'short' ,
112- day : 'numeric' ,
113- year : 'numeric' ,
114- } ) }
115- </ time >
116- < meta itemProp = 'dateModified' content = { post . updated ?? post . date } />
117- </ div >
118- < div className = 'flex-1' >
119- < p className = 'm-0 block translate-y-[-4px] font-[400] text-[var(--landing-text-muted)] text-lg leading-[1.5] sm:text-[20px] md:text-[26px]' >
120- { post . description }
121- </ p >
122- </ div >
123- </ div >
124121 </ header >
125122
126- < div className = 'mx-auto max-w-[900px] px-6 pb-20 sm:px-8 md:px-12' itemProp = 'articleBody' >
127- < div className = 'prose prose-lg prose-invert max-w-none prose-blockquote:border-[var(--landing-border-strong)] prose-hr:border-[var(--landing-bg-elevated)] prose-a:text-[var(--landing-text)] prose-blockquote:text-[var(--landing-text-muted)] prose-code:text-[var(--landing-text)] prose-headings:text-[var(--landing-text)] prose-li:text-[var(--landing-text-muted)] prose-p:text-[var(--landing-text-muted)] prose-strong:text-[var(--landing-text)]' >
128- < Article />
129- { post . faq && post . faq . length > 0 ? < FAQ items = { post . faq } /> : null }
123+ < div className = 'mt-8 h-px w-full bg-[var(--landing-bg-elevated)]' />
124+
125+ < div className = 'mx-5 border-[var(--landing-bg-elevated)] border-x lg:mx-16' >
126+ < div className = 'mx-auto max-w-[900px] px-6 py-16' itemProp = 'articleBody' >
127+ < div className = 'prose prose-lg prose-invert max-w-none prose-blockquote:border-[var(--landing-border-strong)] prose-hr:border-[var(--landing-bg-elevated)] prose-headings:font-[430] prose-headings:font-season prose-a:text-white prose-blockquote:text-[var(--landing-text-muted)] prose-code:text-white prose-headings:text-white prose-li:text-[var(--landing-text-body)] prose-p:text-[var(--landing-text-body)] prose-strong:text-white prose-headings:tracking-[-0.02em]' >
128+ < Article />
129+ { post . faq && post . faq . length > 0 ? < FAQ items = { post . faq } /> : null }
130+ </ div >
130131 </ div >
131- </ div >
132- { related . length > 0 && (
133- < div className = 'mx-auto max-w-[900px] px-6 pb-24 sm:px-8 md:px-12' >
134- < h2 className = 'mb-4 font-[500] text-[24px] text-[var(--landing-text)]' > Related posts</ h2 >
135- < div className = 'grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3' >
136- { related . map ( ( p ) => (
137- < Link key = { p . slug } href = { `/blog/${ p . slug } ` } className = 'group' >
138- < div className = 'overflow-hidden rounded-lg border border-[var(--landing-bg-elevated)]' >
139- < Image
140- src = { p . ogImage }
141- alt = { p . title }
142- width = { 600 }
143- height = { 315 }
144- className = 'h-[160px] w-full object-cover'
145- sizes = '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw'
146- loading = 'lazy'
147- unoptimized
148- />
149- < div className = 'p-3' >
150- < div className = 'mb-1 text-[var(--landing-text-muted)] text-xs' >
132+
133+ { related . length > 0 && (
134+ < >
135+ < div className = 'h-px w-full bg-[var(--landing-bg-elevated)]' />
136+ < nav aria-label = 'Related posts' className = 'flex' >
137+ { related . map ( ( p ) => (
138+ < Link
139+ key = { p . slug }
140+ href = { `/blog/${ p . slug } ` }
141+ className = 'group flex flex-1 flex-col gap-4 border-[var(--landing-bg-elevated)] p-6 transition-colors hover:bg-[var(--landing-bg-elevated)] md:border-l md:first:border-l-0'
142+ >
143+ < div className = 'relative aspect-video w-full overflow-hidden rounded-[5px]' >
144+ < Image
145+ src = { p . ogImage }
146+ alt = { p . title }
147+ fill
148+ sizes = '(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw'
149+ className = 'object-cover'
150+ loading = 'lazy'
151+ unoptimized
152+ />
153+ </ div >
154+ < div className = 'flex flex-col gap-2' >
155+ < span className = 'font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]' >
151156 { new Date ( p . date ) . toLocaleDateString ( 'en-US' , {
152157 month : 'short' ,
153- day : 'numeric' ,
154- year : 'numeric' ,
158+ year : '2-digit' ,
155159 } ) }
156- </ div >
157- < div className = 'font-[500] text-[var(--landing- text)] text-sm leading-tight' >
160+ </ span >
161+ < h3 className = 'font-[430] font-season text-lg text-white leading-tight tracking-[-0.01em] ' >
158162 { p . title }
159- </ div >
163+ </ h3 >
164+ < p className = 'line-clamp-2 text-[var(--landing-text-muted)] text-sm leading-[150%]' >
165+ { p . description }
166+ </ p >
160167 </ div >
161- </ div >
162- </ Link >
163- ) ) }
164- </ div >
165- </ div >
166- ) }
168+ </ Link >
169+ ) ) }
170+ </ nav >
171+ </ >
172+ ) }
173+ </ div >
174+
175+ < div className = '-mt-px h-px w-full bg-[var(--landing-bg-elevated)]' />
176+
167177 < meta itemProp = 'publisher' content = 'Sim' />
168178 < meta itemProp = 'inLanguage' content = 'en-US' />
169179 < meta itemProp = 'keywords' content = { post . tags . join ( ', ' ) } />
180+ { post . wordCount && < meta itemProp = 'wordCount' content = { String ( post . wordCount ) } /> }
170181 </ article >
171182 )
172183}
0 commit comments