11import { ReactNode } from 'react'
2+ import { slug as slugify } from 'github-slugger'
23import { CoreContent } from 'pliny/utils/contentlayer'
34import type { Blog , Authors } from 'contentlayer/generated'
45import Comments from '@/components/Comments'
56import Link from '@/components/Link'
6- import PageTitle from '@/components/PageTitle'
77import SectionContainer from '@/components/SectionContainer'
88import Image from '@/components/Image'
9- import Tag from '@/components/Tag '
9+ import SocialIcon from '@/components/social-icons '
1010import siteMetadata from '@/data/siteMetadata'
1111import ScrollTopAndComment from '@/components/ScrollTopAndComment'
1212
@@ -29,150 +29,195 @@ interface LayoutProps {
2929 children : ReactNode
3030}
3131
32+ const formatAuthorMeta = ( author : CoreContent < Authors > ) => {
33+ if ( author . occupation && author . company ) {
34+ return `${ author . occupation } @ ${ author . company } `
35+ }
36+
37+ return author . occupation || author . company || null
38+ }
39+
40+ const formatAuthorNames = ( authorDetails : CoreContent < Authors > [ ] ) => {
41+ const names = authorDetails . map ( ( author ) => author . name ) . filter ( Boolean )
42+
43+ if ( names . length <= 1 ) {
44+ return names [ 0 ] || null
45+ }
46+
47+ if ( names . length === 2 ) {
48+ return `${ names [ 0 ] } and ${ names [ 1 ] } `
49+ }
50+
51+ return `${ names . slice ( 0 , - 1 ) . join ( ', ' ) } , and ${ names . at ( - 1 ) } `
52+ }
53+
3254export default function PostLayout ( { content, authorDetails, next, prev, children } : LayoutProps ) {
3355 const { filePath, path, slug, date, title, tags } = content
34- const basePath = path . split ( '/' ) [ 0 ]
56+ const byline = formatAuthorNames ( authorDetails )
3557
3658 return (
3759 < SectionContainer >
3860 < ScrollTopAndComment />
3961 < article >
40- < div className = "xl: divide-y xl: divide-gray-200 xl: dark:divide-gray-700" >
41- < header className = "pt-6 xl:pb-6 " >
42- < div className = "space-y-1 text-center " >
43- < dl className = "space-y-10" >
62+ < div className = "divide-y divide-gray-200 dark:divide-gray-700" >
63+ < header className = "pt-6" >
64+ < div className = "mx-auto max-w-3xl space-y-6 pb-10 " >
65+ < dl >
4466 < div >
4567 < dt className = "sr-only" > Published on</ dt >
46- < dd className = "text-base leading-6 font-medium text-gray-500 dark:text-gray-400" >
68+ < dd className = "text-base text-gray-500 dark:text-gray-400" >
69+ < span > on </ span >
4770 < time dateTime = { date } >
4871 { new Date ( date ) . toLocaleDateString ( siteMetadata . locale , postDateTemplate ) }
4972 </ time >
5073 </ dd >
5174 </ div >
5275 </ dl >
53- < div >
54- < PageTitle > { title } </ PageTitle >
55- </ div >
56- </ div >
57- </ header >
58- < div className = "grid-rows-[auto_1fr] divide-y divide-gray-200 pb-8 xl:grid xl:grid-cols-4 xl:gap-x-6 xl:divide-y-0 dark:divide-gray-700" >
59- < dl className = "pt-6 pb-10 xl:border-b xl:border-gray-200 xl:pt-11 xl:dark:border-gray-700" >
60- < dt className = "sr-only" > Authors</ dt >
61- < dd >
62- < ul className = "flex flex-wrap justify-center gap-4 sm:space-x-12 xl:block xl:space-y-8 xl:space-x-0" >
63- { authorDetails . map ( ( author ) => (
64- < li className = "flex items-center space-x-2" key = { author . name } >
65- { author . avatar && (
66- < Image
67- src = { author . avatar }
68- width = { 38 }
69- height = { 38 }
70- alt = "avatar"
71- className = "h-10 w-10 rounded-full"
72- />
73- ) }
74- < dl className = "text-sm leading-5 font-medium whitespace-nowrap" >
75- < dt className = "sr-only" > Name</ dt >
76- < dd className = "text-gray-900 dark:text-gray-100" > { author . name } </ dd >
77- < dt className = "sr-only" > Twitter</ dt >
78- < dd >
79- { author . twitter && (
80- < Link
81- href = { author . twitter }
82- className = "text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
83- >
84- { author . twitter
85- . replace ( 'https://twitter.com/' , '@' )
86- . replace ( 'https://x.com/' , '@' ) }
87- </ Link >
88- ) }
89- </ dd >
90- < dt className = "sr-only" > YouTube</ dt >
91- < dd >
92- { author . youtube && (
93- < Link
94- href = { author . youtube }
95- className = "text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
96- >
97- { author . youtube
98- . replace ( 'https://www.youtube.com/' , '' )
99- . replace ( 'https://youtube.com/' , '' ) }
100- </ Link >
101- ) }
102- </ dd >
103- </ dl >
104- </ li >
76+ < h1 className = "max-w-4xl text-4xl leading-[1] font-semibold tracking-tight text-gray-950 sm:text-5xl md:text-6xl dark:text-gray-50" >
77+ { title }
78+ </ h1 >
79+ { byline && (
80+ < p className = "text-base text-gray-600 dark:text-gray-400" >
81+ By < span className = "font-medium text-gray-900 dark:text-gray-100" > { byline } </ span >
82+ </ p >
83+ ) }
84+ { tags && tags . length > 0 && (
85+ < div className = "flex flex-wrap gap-3" >
86+ { tags . map ( ( tag ) => (
87+ < Link
88+ key = { tag }
89+ href = { `/tags/${ slugify ( tag ) } ` }
90+ className = "rounded-full border border-gray-300 px-4 py-1.5 text-sm font-medium text-gray-700 transition hover:border-gray-400 hover:text-gray-950 dark:border-gray-700 dark:text-gray-300 dark:hover:border-gray-500 dark:hover:text-gray-100"
91+ >
92+ { tag }
93+ </ Link >
10594 ) ) }
106- </ ul >
107- </ dd >
108- </ dl >
109- < div className = "divide-y divide-gray-200 xl:col-span-3 xl:row-span-2 xl:pb-0 dark:divide-gray-700" >
110- < div className = "prose dark:prose-invert max-w-none pt-10 pb-8" > { children } </ div >
111- < div className = "pt-6 pb-6 text-sm text-gray-700 dark:text-gray-300" >
112- < Link href = { discussUrl ( path ) } rel = "nofollow" >
113- Discuss on Twitter
114- </ Link >
115- { ` • ` }
116- < Link href = { editUrl ( filePath ) } > View on GitHub</ Link >
117- </ div >
118- { siteMetadata . comments && (
119- < div
120- className = "pt-6 pb-6 text-center text-gray-700 dark:text-gray-300"
121- id = "comment"
122- >
123- < Comments slug = { slug } />
12495 </ div >
12596 ) }
12697 </ div >
127- < footer >
128- < div className = "divide-gray-200 text-sm leading-5 font-medium xl:col-start-1 xl:row-start-2 xl:divide-y dark:divide-gray-700" >
129- { tags && (
130- < div className = "py-4 xl:py-8" >
131- < h2 className = "text-xs tracking-wide text-gray-500 uppercase dark:text-gray-400" >
132- Tags
133- </ h2 >
134- < div className = "flex flex-wrap" >
135- { tags . map ( ( tag ) => (
136- < Tag key = { tag } text = { tag } />
137- ) ) }
138- </ div >
139- </ div >
140- ) }
98+ </ header >
99+ < div className = "mx-auto max-w-3xl pb-12" >
100+ < div className = "prose dark:prose-invert max-w-none py-10" > { children } </ div >
101+ < footer className = "space-y-10 border-t border-gray-200 pt-10 dark:border-gray-700" >
102+ < div className = "space-y-6" >
103+ < div className = "flex flex-wrap gap-x-4 gap-y-2 text-sm text-gray-700 dark:text-gray-300" >
104+ < Link href = { discussUrl ( path ) } rel = "nofollow" >
105+ Discuss on Twitter
106+ </ Link >
107+ < Link href = { editUrl ( filePath ) } > View on GitHub</ Link >
108+ </ div >
109+ </ div >
110+ { authorDetails . length > 0 && (
111+ < section className = "space-y-6" >
112+ < h2 className = "text-2xl font-semibold tracking-tight text-gray-900 dark:text-gray-100" >
113+ Author
114+ </ h2 >
115+ < ul className = "space-y-6" >
116+ { authorDetails . map ( ( author ) => {
117+ const authorMeta = formatAuthorMeta ( author )
118+ const hasSocials = Boolean (
119+ author . email ||
120+ author . github ||
121+ author . linkedin ||
122+ author . twitter ||
123+ author . youtube ||
124+ author . bluesky
125+ )
126+
127+ return (
128+ < li
129+ className = "rounded-3xl border border-gray-200 bg-gray-50/60 p-6 dark:border-gray-700 dark:bg-gray-900/40"
130+ key = { author . slug || author . name }
131+ >
132+ < div className = "flex flex-col gap-5 sm:flex-row sm:items-center" >
133+ { author . avatar && (
134+ < Image
135+ src = { author . avatar }
136+ width = { 96 }
137+ height = { 96 }
138+ alt = { author . name }
139+ className = "h-20 w-20 rounded-full object-cover"
140+ />
141+ ) }
142+ < div className = "min-w-0 flex-1 space-y-3" >
143+ < div className = "space-y-1" >
144+ < h3 className = "text-xl font-semibold tracking-tight text-gray-900 dark:text-gray-100" >
145+ { author . name }
146+ </ h3 >
147+ { authorMeta && (
148+ < p className = "text-sm text-gray-600 dark:text-gray-400" >
149+ { authorMeta }
150+ </ p >
151+ ) }
152+ </ div >
153+ { author . description && (
154+ < p className = "max-w-2xl text-sm leading-6 text-gray-600 dark:text-gray-300" >
155+ { author . description }
156+ </ p >
157+ ) }
158+ { hasSocials && (
159+ < div className = "flex flex-wrap items-center gap-3" >
160+ < SocialIcon
161+ kind = "mail"
162+ href = { author . email ? `mailto:${ author . email } ` : undefined }
163+ size = { 6 }
164+ />
165+ < SocialIcon kind = "github" href = { author . github } size = { 6 } />
166+ < SocialIcon kind = "linkedin" href = { author . linkedin } size = { 6 } />
167+ < SocialIcon kind = "x" href = { author . twitter } size = { 6 } />
168+ < SocialIcon kind = "youtube" href = { author . youtube } size = { 6 } />
169+ < SocialIcon kind = "bluesky" href = { author . bluesky } size = { 6 } />
170+ </ div >
171+ ) }
172+ </ div >
173+ </ div >
174+ </ li >
175+ )
176+ } ) }
177+ </ ul >
178+ </ section >
179+ ) }
180+ < section className = "space-y-6" >
141181 { ( next || prev ) && (
142- < div className = "flex justify-between py-4 xl:block xl:space-y-8 xl:py-8 " >
182+ < div className = "grid gap-4 md:grid-cols-2 " >
143183 { prev && prev . path && (
144- < div >
145- < h2 className = "text-xs tracking-wide text-gray-500 uppercase dark:text-gray-400" >
146- Previous Article
147- </ h2 >
148- < div className = "text-primary-500 hover:text-primary-600 dark:hover:text-primary-400" >
149- < Link href = { `/${ prev . path } ` } > { prev . title } </ Link >
150- </ div >
184+ < div className = "rounded-3xl border border-gray-200 p-6 dark:border-gray-700" >
185+ < p className = "text-xs tracking-[0.2em] text-gray-500 uppercase dark:text-gray-400" >
186+ Previous article
187+ </ p >
188+ < Link
189+ href = { `/${ prev . path } ` }
190+ className = "hover:text-primary-500 dark:hover:text-primary-400 mt-3 block text-lg font-medium text-gray-900 transition dark:text-gray-100"
191+ >
192+ { prev . title }
193+ </ Link >
151194 </ div >
152195 ) }
153196 { next && next . path && (
154- < div >
155- < h2 className = "text-xs tracking-wide text-gray-500 uppercase dark:text-gray-400" >
156- Next Article
157- </ h2 >
158- < div className = "text-primary-500 hover:text-primary-600 dark:hover:text-primary-400" >
159- < Link href = { `/${ next . path } ` } > { next . title } </ Link >
160- </ div >
197+ < div className = "rounded-3xl border border-gray-200 p-6 dark:border-gray-700" >
198+ < p className = "text-xs tracking-[0.2em] text-gray-500 uppercase dark:text-gray-400" >
199+ Next article
200+ </ p >
201+ < Link
202+ href = { `/${ next . path } ` }
203+ className = "hover:text-primary-500 dark:hover:text-primary-400 mt-3 block text-lg font-medium text-gray-900 transition dark:text-gray-100"
204+ >
205+ { next . title }
206+ </ Link >
161207 </ div >
162208 ) }
163209 </ div >
164210 ) }
165- </ div >
166- < div className = "pt-4 xl:pt-8" >
167- < Link
168- href = { `/${ basePath } ` }
169- className = "text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
170- aria-label = "Back to the blog"
171- >
172- ← Back to the blog
173- </ Link >
174- </ div >
211+ </ section >
175212 </ footer >
213+ { siteMetadata . comments && (
214+ < div
215+ className = "pt-10 text-center text-gray-700 dark:text-gray-300"
216+ id = "comment"
217+ >
218+ < Comments slug = { slug } />
219+ </ div >
220+ ) }
176221 </ div >
177222 </ div >
178223 </ article >
0 commit comments