@@ -6,7 +6,12 @@ import remarkGfm from "remark-gfm"
66import { ArrowLeft } from "lucide-react"
77import { SEO } from "@/lib/seo"
88import { ogImageUrl } from "@/lib/og"
9- import { getBlogPostBySlug , formatPostDatePt } from "@/lib/blog"
9+ import {
10+ getBlogPostBySlug ,
11+ formatPostDatePt ,
12+ getArticleStructuredData ,
13+ getBlogPostBreadcrumbStructuredData ,
14+ } from "@/lib/blog"
1015
1116// Force dynamic rendering to evaluate publish gating at request-time
1217export const dynamic = "force-dynamic"
@@ -72,102 +77,117 @@ export default async function BlogPostPage({ params }: BlogPostPageProps) {
7277 notFound ( )
7378 }
7479
80+ // Generate structured data for SEO
81+ const articleSchema = getArticleStructuredData ( post )
82+ const breadcrumbSchema = getBlogPostBreadcrumbStructuredData ( post )
83+
7584 return (
76- < div className = "container mx-auto px-4 py-12 sm:px-6 lg:px-8" >
77- < article className = "mx-auto max-w-3xl" >
78- { /* Back link */ }
79- < Link
80- href = "/blog"
81- className = "mb-8 inline-flex items-center text-sm text-muted-foreground hover:text-foreground" >
82- < ArrowLeft className = "mr-2 size-4" />
83- Back to Blog
84- </ Link >
85+ < >
86+ { /* JSON-LD Structured Data */ }
87+ < script type = "application/ld+json" dangerouslySetInnerHTML = { { __html : JSON . stringify ( articleSchema ) } } />
88+ < script type = "application/ld+json" dangerouslySetInnerHTML = { { __html : JSON . stringify ( breadcrumbSchema ) } } />
8589
86- { /* Post Header */ }
87- < header className = "mb-8" >
88- < h1 className = "text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl" > { post . title } </ h1 >
89- < div className = "mt-4 flex flex-wrap items-center gap-4 text-sm text-muted-foreground" >
90- < time dateTime = { post . publish_date } > Posted { formatPostDatePt ( post . publish_date ) } </ time >
91- { post . tags . length > 0 && (
92- < >
93- < span className = "text-border" > •</ span >
94- < div className = "flex flex-wrap gap-2" >
95- { post . tags . map ( ( tag ) => (
96- < span key = { tag } className = "rounded-full bg-muted px-2 py-0.5 text-xs" >
97- { tag }
98- </ span >
99- ) ) }
100- </ div >
101- </ >
102- ) }
103- </ div >
104- </ header >
90+ < div className = "container mx-auto px-4 py-12 sm:px-6 lg:px-8" >
91+ < article className = "mx-auto max-w-3xl" >
92+ { /* Back link */ }
93+ < Link
94+ href = "/blog"
95+ className = "mb-8 inline-flex items-center text-sm text-muted-foreground hover:text-foreground" >
96+ < ArrowLeft className = "mr-2 size-4" />
97+ Back to Blog
98+ </ Link >
10599
106- { /* Post Content */ }
107- < div className = "prose prose-lg max-w-none dark:prose-invert" >
108- < ReactMarkdown
109- remarkPlugins = { [ remarkGfm ] }
110- components = { {
111- // Do not allow raw HTML - all components render safely
112- h1 : ( { ...props } ) => (
113- < h1 className = "text-2xl font-bold tracking-tight sm:text-3xl" { ...props } />
114- ) ,
115- h2 : ( { ...props } ) => < h2 className = "mt-10 text-xl font-bold sm:text-2xl" { ...props } /> ,
116- h3 : ( { ...props } ) => < h3 className = "mt-8 text-lg font-semibold" { ...props } /> ,
117- a : ( { ...props } ) => (
118- < a
119- className = "text-primary hover:underline"
120- target = "_blank"
121- rel = "noopener noreferrer"
122- { ...props }
123- />
124- ) ,
125- blockquote : ( { ...props } ) => (
126- < blockquote
127- className = "border-l-4 border-primary/50 pl-4 italic text-muted-foreground"
128- { ...props }
129- />
130- ) ,
131- code : ( { className, children, ...props } ) => {
132- // Check if this is an inline code or code block
133- const isInline = ! className
134- if ( isInline ) {
100+ { /* Post Header */ }
101+ < header className = "mb-8" >
102+ < h1 className = "text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl" > { post . title } </ h1 >
103+ < div className = "mt-4 flex flex-wrap items-center gap-4 text-sm text-muted-foreground" >
104+ < time dateTime = { post . publish_date } > Posted { formatPostDatePt ( post . publish_date ) } </ time >
105+ { post . tags . length > 0 && (
106+ < >
107+ < span className = "text-border" > •</ span >
108+ < div className = "flex flex-wrap gap-2" >
109+ { post . tags . map ( ( tag ) => (
110+ < span key = { tag } className = "rounded-full bg-muted px-2 py-0.5 text-xs" >
111+ { tag }
112+ </ span >
113+ ) ) }
114+ </ div >
115+ </ >
116+ ) }
117+ </ div >
118+ </ header >
119+
120+ { /* Post Content */ }
121+ < div className = "prose prose-lg max-w-none dark:prose-invert" >
122+ < ReactMarkdown
123+ remarkPlugins = { [ remarkGfm ] }
124+ components = { {
125+ // Do not allow raw HTML - all components render safely
126+ h1 : ( { ...props } ) => (
127+ < h1 className = "text-2xl font-bold tracking-tight sm:text-3xl" { ...props } />
128+ ) ,
129+ h2 : ( { ...props } ) => < h2 className = "mt-10 text-xl font-bold sm:text-2xl" { ...props } /> ,
130+ h3 : ( { ...props } ) => < h3 className = "mt-8 text-lg font-semibold" { ...props } /> ,
131+ a : ( { ...props } ) => (
132+ < a
133+ className = "text-primary hover:underline"
134+ target = "_blank"
135+ rel = "noopener noreferrer"
136+ { ...props }
137+ />
138+ ) ,
139+ blockquote : ( { ...props } ) => (
140+ < blockquote
141+ className = "border-l-4 border-primary/50 pl-4 italic text-muted-foreground"
142+ { ...props }
143+ />
144+ ) ,
145+ code : ( { className, children, ...props } ) => {
146+ // Check if this is an inline code or code block
147+ const isInline = ! className
148+ if ( isInline ) {
149+ return (
150+ < code
151+ className = "rounded bg-muted px-1.5 py-0.5 text-sm font-mono"
152+ { ...props } >
153+ { children }
154+ </ code >
155+ )
156+ }
135157 return (
136- < code className = "rounded bg-muted px-1.5 py-0.5 text-sm font-mono" { ...props } >
158+ < code className = { className } { ...props } >
137159 { children }
138160 </ code >
139161 )
140- }
141- return (
142- < code className = { className } { ...props } >
143- { children }
144- </ code >
145- )
146- } ,
147- pre : ( { ...props } ) => (
148- < pre className = "overflow-x-auto rounded-lg bg-muted p-4 text-sm font-mono" { ...props } />
149- ) ,
150- ul : ( { ...props } ) => < ul className = "list-disc pl-6" { ...props } /> ,
151- ol : ( { ...props } ) => < ol className = "list-decimal pl-6" { ...props } /> ,
152- li : ( { ...props } ) => < li className = "mt-2" { ...props } /> ,
153- p : ( { ...props } ) => < p className = "mt-4 leading-7" { ...props } /> ,
154- strong : ( { ...props } ) => < strong className = "font-semibold" { ...props } /> ,
155- hr : ( { ...props } ) => < hr className = "my-8 border-border" { ...props } /> ,
156- } } >
157- { post . content }
158- </ ReactMarkdown >
159- </ div >
162+ } ,
163+ pre : ( { ...props } ) => (
164+ < pre
165+ className = "overflow-x-auto rounded-lg bg-muted p-4 text-sm font-mono"
166+ { ...props }
167+ />
168+ ) ,
169+ ul : ( { ...props } ) => < ul className = "list-disc pl-6" { ...props } /> ,
170+ ol : ( { ...props } ) => < ol className = "list-decimal pl-6" { ...props } /> ,
171+ li : ( { ...props } ) => < li className = "mt-2" { ...props } /> ,
172+ p : ( { ...props } ) => < p className = "mt-4 leading-7" { ...props } /> ,
173+ strong : ( { ...props } ) => < strong className = "font-semibold" { ...props } /> ,
174+ hr : ( { ...props } ) => < hr className = "my-8 border-border" { ...props } /> ,
175+ } } >
176+ { post . content }
177+ </ ReactMarkdown >
178+ </ div >
160179
161- { /* Footer */ }
162- < footer className = "mt-12 border-t border-border pt-8" >
163- < Link
164- href = "/blog"
165- className = "inline-flex items-center text-sm text-muted-foreground hover:text-foreground" >
166- < ArrowLeft className = "mr-2 size-4" />
167- Back to Blog
168- </ Link >
169- </ footer >
170- </ article >
171- </ div >
180+ { /* Footer */ }
181+ < footer className = "mt-12 border-t border-border pt-8" >
182+ < Link
183+ href = "/blog"
184+ className = "inline-flex items-center text-sm text-muted-foreground hover:text-foreground" >
185+ < ArrowLeft className = "mr-2 size-4" />
186+ Back to Blog
187+ </ Link >
188+ </ footer >
189+ </ article >
190+ </ div >
191+ </ >
172192 )
173193}
0 commit comments