Skip to content

Commit 3b711bd

Browse files
committed
refactor: add blog categories & improving design
1 parent 651f5c8 commit 3b711bd

File tree

22 files changed

+629
-328
lines changed

22 files changed

+629
-328
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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+
}

app/(marketing)/blog/[slug]/page.tsx

Lines changed: 0 additions & 130 deletions
This file was deleted.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { allPosts } from "contentlayer/generated";
2+
import { Metadata } from "next";
3+
import { notFound } from "next/navigation";
4+
5+
import { BlogCard } from "@/components/content/blog-card";
6+
import { BLOG_CATEGORIES } from "@/config/blog";
7+
import { constructMetadata } from "@/lib/utils";
8+
9+
export async function generateStaticParams() {
10+
return BLOG_CATEGORIES.map((category) => ({
11+
slug: category.slug,
12+
}));
13+
}
14+
15+
export async function generateMetadata({
16+
params,
17+
}: {
18+
params: { slug: string };
19+
}): Promise<Metadata | undefined> {
20+
const category = BLOG_CATEGORIES.find(
21+
(category) => category.slug === params.slug,
22+
);
23+
if (!category) {
24+
return;
25+
}
26+
27+
const { title, description } = category;
28+
29+
return constructMetadata({
30+
title: `${title} Posts – Next SaaS Starter`,
31+
description,
32+
});
33+
}
34+
35+
export default async function BlogCategory({
36+
params,
37+
}: {
38+
params: {
39+
slug: string;
40+
};
41+
}) {
42+
const data = BLOG_CATEGORIES.find(
43+
(category) => category.slug === params.slug,
44+
);
45+
46+
if (!data) {
47+
notFound();
48+
}
49+
50+
const articles = allPosts
51+
.filter((post) => post.categories.includes(data.slug))
52+
.sort((a, b) => b.date.localeCompare(a.date));
53+
54+
return (
55+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
56+
{articles.map((article, idx) => (
57+
<BlogCard key={article._id} data={article} priority={idx <= 2} />
58+
))}
59+
</div>
60+
);
61+
}

app/(marketing)/blog/layout.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { BlogHeaderLayout } from "@/components/content/blog-header-layout";
2+
import MaxWidthWrapper from "@/components/shared/max-width-wrapper";
3+
4+
export default function BlogLayout({
5+
children,
6+
}: {
7+
children: React.ReactNode;
8+
}) {
9+
return (
10+
<>
11+
<BlogHeaderLayout />
12+
<MaxWidthWrapper className="pb-16">{children}</MaxWidthWrapper>
13+
</>
14+
);
15+
}

0 commit comments

Comments
 (0)