|
| 1 | +import type { Metadata } from "next"; |
| 2 | +import { getAllPosts, getPostBySlug, FALLBACK_IMAGE } from "@/lib/posts"; |
| 3 | +import { format } from "date-fns"; |
| 4 | +import Link from "next/link"; |
| 5 | +import ShareButtons from "@/components/ShareButtons"; |
| 6 | +import { notFound } from "next/navigation"; |
| 7 | + |
| 8 | +export async function generateStaticParams() { |
| 9 | + const posts = getAllPosts(); |
| 10 | + return posts.map((p) => ({ slug: p.slug })); |
| 11 | +} |
| 12 | + |
| 13 | +export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> { |
| 14 | + const { slug } = await params; |
| 15 | + const post = await getPostBySlug(slug); |
| 16 | + if (!post) return { title: "Post Not Found" }; |
| 17 | + return { title: post.title, description: post.abstract }; |
| 18 | +} |
| 19 | + |
| 20 | +export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) { |
| 21 | + const { slug } = await params; |
| 22 | + const post = await getPostBySlug(slug); |
| 23 | + if (!post) notFound(); |
| 24 | + |
| 25 | + const allPosts = getAllPosts(); |
| 26 | + const recentPosts = allPosts.filter((p) => p.slug !== slug).slice(0, 4); |
| 27 | + |
| 28 | + return ( |
| 29 | + <div className="mx-auto flex"> |
| 30 | + <article className="w-full break-words"> |
| 31 | + {/* Hero */} |
| 32 | + <div id="hero" className="bg-gradient-to-br from-red-dark via-red to-red relative"> |
| 33 | + <div className="container pt-14 pb-12 sm:py-[100px] text-white"> |
| 34 | + <h1 className="font-medium text-xl sm:text-2xl leading-none relative mb-6 sm:mb-5">{post.title}</h1> |
| 35 | + <div className="flex flex-wrap gap-6"> |
| 36 | + {post.authors.map((author, i) => ( |
| 37 | + <AuthorBlock key={i} author={author} date={post.date} duration={post.duration} /> |
| 38 | + ))} |
| 39 | + </div> |
| 40 | + </div> |
| 41 | + </div> |
| 42 | + |
| 43 | + {/* Content */} |
| 44 | + <div className="container py-14 sm:py-[80px] lg:py-[90px]"> |
| 45 | + <main className="w-full min-w-0 max-w-[800px] mx-auto"> |
| 46 | + <div |
| 47 | + className="content text-sm text-charcoal font-normal sm:text-base" |
| 48 | + dangerouslySetInnerHTML={{ __html: post.contentHtml }} |
| 49 | + /> |
| 50 | + <div className="mt-11 mx-auto w-fit"> |
| 51 | + <ShareButtons /> |
| 52 | + </div> |
| 53 | + </main> |
| 54 | + </div> |
| 55 | + |
| 56 | + {/* Recent Posts */} |
| 57 | + {recentPosts.length > 0 && ( |
| 58 | + <div className="w-full bg-gray-light"> |
| 59 | + <div className="container py-[60px] sm:py-[110px] text-black"> |
| 60 | + <h2 className="text-[24px] leading-none font-medium">Recent Hiero Posts</h2> |
| 61 | + <ul className="mt-6 grid grid-cols-1 xl:grid-cols-4 gap-[38px] list-none p-0"> |
| 62 | + {recentPosts.map((rp) => ( |
| 63 | + <li key={rp.slug}> |
| 64 | + <Link href={`/blog/${rp.slug}`} className="no-underline grid grid-cols-1 sm:grid-cols-2 sm:gap-9 xl:gap-0 xl:grid-cols-1"> |
| 65 | + {/* eslint-disable-next-line @next/next/no-img-element */} |
| 66 | + <img src={rp.featuredImage} alt={rp.title} className="w-full h-[140px] object-cover" loading="lazy" /> |
| 67 | + <div> |
| 68 | + <h3 className="mt-3 sm:mt-0 xl:mt-3 text-[20px] font-medium text-black line-clamp-1">{rp.title}</h3> |
| 69 | + <p className="text-charcoal text-sm font-normal mt-1 leading-none"> |
| 70 | + {rp.duration}{rp.duration && <span className="mx-1">•</span>} |
| 71 | + {format(new Date(rp.date), "MMMM d, yyyy")} |
| 72 | + </p> |
| 73 | + {rp.abstract && ( |
| 74 | + <p className="text-charcoal text-sm sm:text-base font-normal line-clamp-4 xl:line-clamp-2 mt-2"> |
| 75 | + {rp.abstract.length > 400 ? rp.abstract.slice(0, 400) + "…" : rp.abstract} |
| 76 | + </p> |
| 77 | + )} |
| 78 | + </div> |
| 79 | + </Link> |
| 80 | + </li> |
| 81 | + ))} |
| 82 | + </ul> |
| 83 | + </div> |
| 84 | + </div> |
| 85 | + )} |
| 86 | + </article> |
| 87 | + </div> |
| 88 | + ); |
| 89 | +} |
| 90 | + |
| 91 | +function AuthorBlock({ author, date, duration }: { author: { name?: string; title?: string; organization?: string; link?: string; image?: string }; date: string; duration?: string }) { |
| 92 | + const inner = ( |
| 93 | + <> |
| 94 | + {author.image && ( |
| 95 | + // eslint-disable-next-line @next/next/no-img-element |
| 96 | + <img src={author.image} alt={author.name ?? ""} className="inline-block h-[72px] w-[72px] rounded-full bg-white" loading="lazy" /> |
| 97 | + )} |
| 98 | + <div className="font-normal"> |
| 99 | + <p className="m-0"> |
| 100 | + {duration}{duration && <span className="mx-1">•</span>} |
| 101 | + {format(new Date(date), "MMMM d, yyyy")} |
| 102 | + </p> |
| 103 | + <p className="m-0">by {author.name}</p> |
| 104 | + {(author.title || author.organization) && ( |
| 105 | + <p className="m-0"> |
| 106 | + {author.title}{author.title && author.organization ? ", " : " "}{author.organization} |
| 107 | + </p> |
| 108 | + )} |
| 109 | + </div> |
| 110 | + </> |
| 111 | + ); |
| 112 | + |
| 113 | + if (author.link) { |
| 114 | + return ( |
| 115 | + <a href={author.link} target="_blank" rel="noopener noreferrer" className="inline-flex items-center text-sand text-sm gap-x-4 no-underline" title={author.name}> |
| 116 | + {inner} |
| 117 | + </a> |
| 118 | + ); |
| 119 | + } |
| 120 | + |
| 121 | + return ( |
| 122 | + <span className="inline-flex items-center text-sand text-sm gap-x-4"> |
| 123 | + {inner} |
| 124 | + </span> |
| 125 | + ); |
| 126 | +} |
0 commit comments