@@ -187,15 +187,15 @@ export default async function PostDetails({
187187 const post : Post | null = await client
188188 . withConfig ( { useCdn : false } )
189189 . fetch ( query , {
190- slug : slugParam ,
191- locale,
192- } ) ;
190+ slug : slugParam ,
191+ locale,
192+ } ) ;
193193 const recommendedAll : Post [ ] = await client
194194 . withConfig ( { useCdn : false } )
195195 . fetch ( recommendedQuery , {
196- slug : slugParam ,
197- locale,
198- } ) ;
196+ slug : slugParam ,
197+ locale,
198+ } ) ;
199199 const recommendedPosts = seededShuffle (
200200 recommendedAll ,
201201 hashString ( slugParam )
@@ -212,9 +212,71 @@ export default async function PostDetails({
212212 ] . filter ( Boolean ) as string [ ] ;
213213 const authorMeta = authorMetaParts . join ( ' · ' ) ;
214214 const categoryLabel = post . categories ?. [ 0 ] ;
215+ const baseUrl = process . env . NEXT_PUBLIC_SITE_URL ;
216+ const postUrl = baseUrl
217+ ? `${ baseUrl } /${ locale } /blog/${ slugParam } `
218+ : null ;
219+ const blogUrl = baseUrl ? `${ baseUrl } /${ locale } /blog` : null ;
220+ const description = plainTextFromPortableText ( post . body ) . slice ( 0 , 160 ) ;
221+ const breadcrumbsJsonLd =
222+ blogUrl && postUrl
223+ ? {
224+ '@context' : 'https://schema.org' ,
225+ '@type' : 'BreadcrumbList' ,
226+ itemListElement : [
227+ {
228+ '@type' : 'ListItem' ,
229+ position : 1 ,
230+ name : tNav ( 'blog' ) ,
231+ item : blogUrl ,
232+ } ,
233+ {
234+ '@type' : 'ListItem' ,
235+ position : 2 ,
236+ name : post . title ,
237+ item : postUrl ,
238+ } ,
239+ ] ,
240+ }
241+ : null ;
242+ const articleJsonLd =
243+ postUrl
244+ ? {
245+ '@context' : 'https://schema.org' ,
246+ '@type' : 'BlogPosting' ,
247+ headline : post . title ,
248+ description : description || undefined ,
249+ mainEntityOfPage : postUrl ,
250+ url : postUrl ,
251+ datePublished : post . publishedAt || undefined ,
252+ author : post . author ?. name
253+ ? {
254+ '@type' : 'Person' ,
255+ name : post . author . name ,
256+ }
257+ : undefined ,
258+ image : post . mainImage ? [ post . mainImage ] : undefined ,
259+ }
260+ : null ;
215261
216262 return (
217263 < main className = "max-w-3xl mx-auto px-6 py-12" >
264+ { breadcrumbsJsonLd && (
265+ < script
266+ type = "application/ld+json"
267+ dangerouslySetInnerHTML = { {
268+ __html : JSON . stringify ( breadcrumbsJsonLd ) ,
269+ } }
270+ />
271+ ) }
272+ { articleJsonLd && (
273+ < script
274+ type = "application/ld+json"
275+ dangerouslySetInnerHTML = { {
276+ __html : JSON . stringify ( articleJsonLd ) ,
277+ } }
278+ />
279+ ) }
218280 < div className = "mb-6 relative left-1/2 right-1/2 w-screen -translate-x-1/2 px-6" >
219281 < div className = "mx-auto flex max-w-6xl justify-start" >
220282 < div className = "flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400" >
@@ -255,7 +317,11 @@ export default async function PostDetails({
255317 </ Link >
256318 ) }
257319 { authorName && post . publishedAt && < span > ·</ span > }
258- { post . publishedAt && < span > { formatBlogDate ( post . publishedAt ) } </ span > }
320+ { post . publishedAt && (
321+ < time dateTime = { post . publishedAt } >
322+ { formatBlogDate ( post . publishedAt ) }
323+ </ time >
324+ ) }
259325 </ div >
260326 ) }
261327
@@ -328,30 +394,34 @@ export default async function PostDetails({
328394 />
329395 </ div >
330396 ) }
331- < h3 className = "mt-4 text-lg font-semibold text-gray-900 transition group-hover:underline underline-offset-4 dark:text-gray-100" >
332- { item . title }
333- </ h3 >
334- { item . body && (
335- < p className = "mt-2 text-sm leading-relaxed text-gray-600 dark:text-gray-400 line-clamp-2" >
336- { plainTextFromPortableText ( item . body ) }
337- </ p >
338- ) }
339- { ( item . author ?. name || item . publishedAt ) && (
340- < div className = "mt-auto pt-3 flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400" >
341- { item . author ?. image && (
342- < span className = "relative h-5 w-5 overflow-hidden rounded-full" >
343- < Image
344- src = { item . author . image }
397+ < h3 className = "mt-4 text-lg font-semibold text-gray-900 transition group-hover:underline underline-offset-4 dark:text-gray-100" >
398+ { item . title }
399+ </ h3 >
400+ { item . body && (
401+ < p className = "mt-2 text-sm leading-relaxed text-gray-600 dark:text-gray-400 line-clamp-2" >
402+ { plainTextFromPortableText ( item . body ) }
403+ </ p >
404+ ) }
405+ { ( item . author ?. name || item . publishedAt ) && (
406+ < div className = "mt-auto pt-3 flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400" >
407+ { item . author ?. image && (
408+ < span className = "relative h-5 w-5 overflow-hidden rounded-full" >
409+ < Image
410+ src = { item . author . image }
345411 alt = { item . author . name || 'Author' }
346412 fill
347413 className = "object-cover"
348414 />
349415 </ span >
350416 ) }
351417 { item . author ?. name && < span > { item . author . name } </ span > }
352- { item . author ?. name && item . publishedAt && < span > ·</ span > }
418+ { item . author ?. name && item . publishedAt && (
419+ < span > ·</ span >
420+ ) }
353421 { item . publishedAt && (
354- < span > { formatBlogDate ( item . publishedAt ) } </ span >
422+ < time dateTime = { item . publishedAt } >
423+ { formatBlogDate ( item . publishedAt ) }
424+ </ time >
355425 ) }
356426 </ div >
357427 ) }
0 commit comments