|
| 1 | +import { allPosts } from "contentlayer/generated"; |
| 2 | +import { notFound } from "next/navigation"; |
| 3 | + |
| 4 | +import { Mdx } from "@/components/content/mdx-components"; |
| 5 | + |
| 6 | +import "@/styles/mdx.css"; |
| 7 | + |
| 8 | +import { Metadata } from "next"; |
| 9 | +import Image from "next/image"; |
| 10 | +import Link from "next/link"; |
| 11 | + |
| 12 | +import Author from "@/components/content/author"; |
| 13 | +import MaxWidthWrapper from "@/components/shared/max-width-wrapper"; |
| 14 | +import { DashboardTableOfContents } from "@/components/shared/toc"; |
| 15 | +import { buttonVariants } from "@/components/ui/button"; |
| 16 | +import { BLOG_CATEGORIES } from "@/config/blog"; |
| 17 | +import { getTableOfContents } from "@/lib/toc"; |
| 18 | +import { cn, constructMetadata, formatDate } from "@/lib/utils"; |
| 19 | + |
| 20 | +export async function generateStaticParams() { |
| 21 | + return allPosts.map((post) => ({ |
| 22 | + slug: post.slugAsParams, |
| 23 | + })); |
| 24 | +} |
| 25 | + |
| 26 | +export async function generateMetadata({ |
| 27 | + params, |
| 28 | +}: { |
| 29 | + params: { slug: string }; |
| 30 | +}): Promise<Metadata | undefined> { |
| 31 | + const post = allPosts.find((post) => post.slugAsParams === params.slug); |
| 32 | + if (!post) { |
| 33 | + return; |
| 34 | + } |
| 35 | + |
| 36 | + const { title, description, image } = post; |
| 37 | + |
| 38 | + return constructMetadata({ |
| 39 | + title: `${title} – SaaS Starter`, |
| 40 | + description: description, |
| 41 | + image, |
| 42 | + }); |
| 43 | +} |
| 44 | + |
| 45 | +export default async function PostPage({ |
| 46 | + params, |
| 47 | +}: { |
| 48 | + params: { |
| 49 | + slug: string; |
| 50 | + }; |
| 51 | +}) { |
| 52 | + const post = allPosts.find((post) => post.slugAsParams === params.slug); |
| 53 | + |
| 54 | + if (!post) { |
| 55 | + notFound(); |
| 56 | + } |
| 57 | + |
| 58 | + const category = BLOG_CATEGORIES.find( |
| 59 | + (category) => category.slug === post.categories[0], |
| 60 | + )!; |
| 61 | + |
| 62 | + const relatedArticles = |
| 63 | + (post.related && |
| 64 | + post.related.map( |
| 65 | + (slug) => allPosts.find((post) => post.slugAsParams === slug)!, |
| 66 | + )) || |
| 67 | + []; |
| 68 | + |
| 69 | + const toc = await getTableOfContents(post.body.raw); |
| 70 | + |
| 71 | + return ( |
| 72 | + <> |
| 73 | + <MaxWidthWrapper className="pt-6 md:pt-10"> |
| 74 | + <div className="flex flex-col space-y-4"> |
| 75 | + <div className="flex items-center space-x-4"> |
| 76 | + <Link |
| 77 | + href={`/blog/category/${category.slug}`} |
| 78 | + className={cn( |
| 79 | + buttonVariants({ |
| 80 | + variant: "outline", |
| 81 | + size: "sm", |
| 82 | + rounded: "lg", |
| 83 | + }), |
| 84 | + "h-8", |
| 85 | + )} |
| 86 | + > |
| 87 | + {category.title} |
| 88 | + </Link> |
| 89 | + <time |
| 90 | + dateTime={post.date} |
| 91 | + className="text-sm font-medium text-muted-foreground" |
| 92 | + > |
| 93 | + {formatDate(post.date)} |
| 94 | + </time> |
| 95 | + </div> |
| 96 | + <h1 className="font-heading text-3xl text-foreground sm:text-4xl"> |
| 97 | + {post.title} |
| 98 | + </h1> |
| 99 | + <p className="text-base text-muted-foreground md:text-lg"> |
| 100 | + {post.description} |
| 101 | + </p> |
| 102 | + <div className="flex flex-nowrap items-center space-x-5 pt-1 md:space-x-8"> |
| 103 | + {post.authors.map((author) => ( |
| 104 | + <Author username={author} key={post._id + author} /> |
| 105 | + ))} |
| 106 | + </div> |
| 107 | + </div> |
| 108 | + </MaxWidthWrapper> |
| 109 | + |
| 110 | + <div className="relative"> |
| 111 | + <div className="absolute top-52 w-full border-t" /> |
| 112 | + |
| 113 | + <MaxWidthWrapper className="grid grid-cols-4 gap-10 pt-8 max-md:px-0"> |
| 114 | + <div className="relative col-span-4 mb-10 flex flex-col space-y-8 bg-background sm:border md:rounded-xl lg:col-span-3"> |
| 115 | + <Image |
| 116 | + className="aspect-[1200/630] border-b object-cover md:rounded-t-xl" |
| 117 | + src={post.image} |
| 118 | + width={1200} |
| 119 | + height={630} |
| 120 | + alt={post.title} |
| 121 | + priority |
| 122 | + /> |
| 123 | + <div className="px-[.8rem] pb-10 md:px-8"> |
| 124 | + <Mdx code={post.body.code} /> |
| 125 | + </div> |
| 126 | + </div> |
| 127 | + |
| 128 | + <div className="sticky top-20 col-span-1 mt-52 hidden flex-col divide-y divide-muted self-start pb-24 lg:flex"> |
| 129 | + <DashboardTableOfContents toc={toc} /> |
| 130 | + </div> |
| 131 | + </MaxWidthWrapper> |
| 132 | + </div> |
| 133 | + |
| 134 | + <MaxWidthWrapper> |
| 135 | + {relatedArticles.length > 0 && ( |
| 136 | + <div className="flex flex-col space-y-4 pb-16"> |
| 137 | + <p className="font-heading text-2xl text-foreground"> |
| 138 | + More Articles |
| 139 | + </p> |
| 140 | + |
| 141 | + <div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:gap-6"> |
| 142 | + {relatedArticles.map((post) => ( |
| 143 | + <Link |
| 144 | + key={post.slug} |
| 145 | + href={post.slug} |
| 146 | + className="flex flex-col space-y-2 rounded-xl border p-5 transition-colors duration-300 hover:bg-muted/80" |
| 147 | + > |
| 148 | + <h3 className="font-heading text-xl text-foreground"> |
| 149 | + {post.title} |
| 150 | + </h3> |
| 151 | + <p className="line-clamp-2 text-[15px] text-muted-foreground"> |
| 152 | + {post.description} |
| 153 | + </p> |
| 154 | + <p className="text-sm text-muted-foreground"> |
| 155 | + {formatDate(post.date)} |
| 156 | + </p> |
| 157 | + </Link> |
| 158 | + ))} |
| 159 | + </div> |
| 160 | + </div> |
| 161 | + )} |
| 162 | + </MaxWidthWrapper> |
| 163 | + </> |
| 164 | + ); |
| 165 | +} |
0 commit comments