@@ -12,11 +12,14 @@ import ReactMarkdown from "react-markdown";
1212import type { Components } from "react-markdown" ;
1313import rehypeRaw from "rehype-raw" ;
1414import remarkGfm from "remark-gfm" ;
15+ import remarkToc from "remark-toc" ;
1516import { codeToHtml } from "shiki" ;
1617import type { BundledLanguage } from "shiki/bundle/web" ;
18+ import slugify from "slugify" ;
1719import TurndownService from "turndown" ;
1820// @ts -ignore
1921import * as turndownPluginGfm from "turndown-plugin-gfm" ;
22+ import { TableOfContents } from "./components/TableOfContents" ;
2023import { ZoomableImage } from "./components/ZoomableImage" ;
2124
2225type Props = {
@@ -158,7 +161,7 @@ export default async function BlogPostPage({ params }: Props) {
158161 const gfm = turndownPluginGfm . gfm ;
159162 const tables = turndownPluginGfm . tables ;
160163 const strikethrough = turndownPluginGfm . strikethrough ;
161- turndownService . use ( [ tables , strikethrough , gfm ] ) ;
164+ turndownService . use ( [ tables , strikethrough , gfm , remarkToc ] ) ;
162165
163166 const markdown = turndownService . turndown ( post . html ) ;
164167
@@ -169,18 +172,45 @@ export default async function BlogPostPage({ params }: Props) {
169172 } ) ;
170173
171174 const components : Partial < Components > = {
172- h1 : ( { node, ...props } ) => (
173- < h1
174- className = "text-xl md:text-2xl xl:text-3xl text-primary font-bold mt-8 mb-4"
175- { ...props }
176- />
177- ) ,
178- h2 : ( { node, ...props } ) => (
179- < h2 className = "text-2xl text-primary/90 font-bold mt-6 mb-3" { ...props } />
180- ) ,
181- h3 : ( { node, ...props } ) => (
182- < h3 className = "text-xl text-primary/90 font-bold mt-4 mb-2" { ...props } />
183- ) ,
175+ h1 : ( { node, ...props } ) => {
176+ const id = slugify ( props . children ?. toString ( ) || "" , {
177+ lower : true ,
178+ strict : true ,
179+ } ) ;
180+ return (
181+ < h1
182+ id = { id }
183+ className = "text-xl md:text-2xl xl:text-3xl text-primary font-bold mt-8 mb-4"
184+ { ...props }
185+ />
186+ ) ;
187+ } ,
188+ h2 : ( { node, ...props } ) => {
189+ const id = slugify ( props . children ?. toString ( ) || "" , {
190+ lower : true ,
191+ strict : true ,
192+ } ) ;
193+ return (
194+ < h2
195+ id = { id }
196+ className = "text-2xl text-primary/90 font-semibold mt-6 mb-3"
197+ { ...props }
198+ />
199+ ) ;
200+ } ,
201+ h3 : ( { node, ...props } ) => {
202+ const id = slugify ( props . children ?. toString ( ) || "" , {
203+ lower : true ,
204+ strict : true ,
205+ } ) ;
206+ return (
207+ < h3
208+ id = { id }
209+ className = "text-xl text-primary/90 font-semibold mt-4 mb-2"
210+ { ...props }
211+ />
212+ ) ;
213+ } ,
184214 p : ( { node, children, ...props } ) => {
185215 return (
186216 < p
@@ -254,7 +284,7 @@ export default async function BlogPostPage({ params }: Props) {
254284 } ;
255285
256286 return (
257- < article className = "container mx-auto px-4 pb-12 max-w-5xl " >
287+ < article className = "mx-auto px-4 sm:px-6 lg:px-8 pb-12 max-w-7xl w-full " >
258288 < Link
259289 href = "/blog"
260290 className = "inline-flex items-center mb-8 text-primary hover:text-primary/80 transition-colors"
@@ -274,95 +304,106 @@ export default async function BlogPostPage({ params }: Props) {
274304 { t ( "backToBlog" ) }
275305 </ Link >
276306
277- < div className = " rounded-lg p-8 shadow-lg border border-border" >
278- < header className = "mb-8" >
279- < h1 className = "text-xl md:text-2xl xl:text-3xl font-bold mb-4" >
280- { post . title }
281- </ h1 >
282- < div className = "flex items-center mb-6" >
283- { post . primary_author ?. profile_image && (
284- < div className = "relative h-12 w-12 rounded-full overflow-hidden mr-4" >
285- { post . primary_author . twitter ? (
286- < a
287- href = { `https://twitter.com/${ post . primary_author . twitter } ` }
288- target = "_blank"
289- rel = "noopener noreferrer"
290- className = "block cursor-pointer transition-opacity hover:opacity-90"
291- >
307+ < div className = "grid grid-cols-1 lg:grid-cols-[1fr_250px] gap-8" >
308+ < div className = "rounded-lg p-8 shadow-lg border border-border" >
309+ < header className = "mb-8" >
310+ < h1 className = "text-xl md:text-2xl xl:text-3xl font-bold mb-4" >
311+ { post . title }
312+ </ h1 >
313+ < div className = "flex items-center mb-6" >
314+ { post . primary_author ?. profile_image && (
315+ < div className = "relative h-12 w-12 rounded-full overflow-hidden mr-4" >
316+ { post . primary_author . twitter ? (
317+ < a
318+ href = { `https://twitter.com/${ post . primary_author . twitter } ` }
319+ target = "_blank"
320+ rel = "noopener noreferrer"
321+ className = "block cursor-pointer transition-opacity hover:opacity-90"
322+ >
323+ < Image
324+ src = { post . primary_author . profile_image }
325+ alt = { post . primary_author . name }
326+ fill
327+ className = "object-cover"
328+ />
329+ </ a >
330+ ) : (
292331 < Image
293332 src = { post . primary_author . profile_image }
294333 alt = { post . primary_author . name }
295334 fill
296335 className = "object-cover"
297336 />
298- </ a >
299- ) : (
300- < Image
301- src = { post . primary_author . profile_image }
302- alt = { post . primary_author . name }
303- fill
304- className = "object-cover"
305- />
306- ) }
337+ ) }
338+ </ div >
339+ ) }
340+ < div >
341+ < p className = "font-medium" >
342+ { post . primary_author ?. twitter ? (
343+ < a
344+ href = { `https://twitter.com/${ post . primary_author . twitter } ` }
345+ target = "_blank"
346+ rel = "noopener noreferrer"
347+ className = "hover:text-primary transition-colors"
348+ >
349+ { post . primary_author . name || "Unknown Author" }
350+ </ a >
351+ ) : (
352+ post . primary_author ?. name || "Unknown Author"
353+ ) }
354+ </ p >
355+ < p className = "text-sm text-muted-foreground" >
356+ { formattedDate } • { post . reading_time } min read
357+ </ p >
307358 </ div >
308- ) }
309- < div >
310- < p className = "font-medium" >
311- { post . primary_author ?. twitter ? (
312- < a
313- href = { `https://twitter.com/${ post . primary_author . twitter } ` }
314- target = "_blank"
315- rel = "noopener noreferrer"
316- className = "hover:text-primary transition-colors"
317- >
318- { post . primary_author . name || "Unknown Author" }
319- </ a >
320- ) : (
321- post . primary_author ?. name || "Unknown Author"
322- ) }
323- </ p >
324- < p className = "text-sm text-muted-foreground" >
325- { formattedDate } • { post . reading_time } min read
326- </ p >
327359 </ div >
360+ { post . feature_image && (
361+ < div className = "relative w-full h-[400px] mb-8" >
362+ < ZoomableImage
363+ src = { post . feature_image }
364+ alt = { post . title }
365+ className = "rounded-lg h-full w-full object-cover"
366+ />
367+ </ div >
368+ ) }
369+ </ header >
370+
371+ < div className = "prose prose-lg max-w-none" >
372+ < ReactMarkdown
373+ remarkPlugins = { [
374+ remarkGfm ,
375+ [ remarkToc , { tight : true , maxDepth : 3 } ] ,
376+ ] }
377+ rehypePlugins = { [ rehypeRaw ] }
378+ components = { components }
379+ >
380+ { markdown }
381+ </ ReactMarkdown >
328382 </ div >
329- { post . feature_image && (
330- < div className = "relative w-full h-[400px] mb-8" >
331- < ZoomableImage
332- src = { post . feature_image }
333- alt = { post . title }
334- className = "rounded-lg h-full w-full object-cover"
335- />
383+
384+ { post . tags && post . tags . length > 0 && (
385+ < div className = "mt-12 pt-6 border-t border-border" >
386+ < h2 className = "text-xl font-semibold mb-4" > { t ( "tags" ) } </ h2 >
387+ < div className = "flex flex-wrap gap-2" >
388+ { post . tags . map ( ( tag ) => (
389+ < Link
390+ key = { tag . id }
391+ href = { `/blog/tag/${ tag . slug } ` }
392+ className = "px-4 py-2 bg-muted hover:bg-muted/80 rounded-full text-sm transition-colors"
393+ >
394+ { tag . name }
395+ </ Link >
396+ ) ) }
397+ </ div >
336398 </ div >
337399 ) }
338- </ header >
339-
340- < div className = "prose prose-lg max-w-none" >
341- < ReactMarkdown
342- remarkPlugins = { [ remarkGfm ] }
343- rehypePlugins = { [ rehypeRaw ] }
344- components = { components }
345- >
346- { markdown }
347- </ ReactMarkdown >
348400 </ div >
349401
350- { post . tags && post . tags . length > 0 && (
351- < div className = "mt-12 pt-6 border-t border-border" >
352- < h2 className = "text-xl font-semibold mb-4" > { t ( "tags" ) } </ h2 >
353- < div className = "flex flex-wrap gap-2" >
354- { post . tags . map ( ( tag ) => (
355- < Link
356- key = { tag . id }
357- href = { `/blog/tag/${ tag . slug } ` }
358- className = "px-4 py-2 bg-muted hover:bg-muted/80 rounded-full text-sm transition-colors"
359- >
360- { tag . name }
361- </ Link >
362- ) ) }
363- </ div >
402+ < div className = "hidden lg:block max-w-[16rem]" >
403+ < div className = "sticky top-4" >
404+ < TableOfContents />
364405 </ div >
365- ) }
406+ </ div >
366407 </ div >
367408
368409 { relatedPosts . length > 0 && (
0 commit comments