Skip to content

Commit d141cf2

Browse files
authored
블로그가 질릴 쯤엔 전면 리팩토링을 합니다 (#1)
* react19, next15로 업데이트 * 게스트북에 무한스크롤 적용 * velite 적용 (#2) * velite 적용 * Permalink 사용으로 블로그 링크 개선 및 사이트맵 수정 * biome 적용 (#3) * Remove rehype-code-titles from dependencies in package.json and pnpm-lock.yaml * 스타일 변경 * 폰트 변경
1 parent ed208e4 commit d141cf2

File tree

103 files changed

+3424
-6215
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

103 files changed

+3424
-6215
lines changed

.eslintrc.json

-3
This file was deleted.

.gitignore

+4-3
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ yarn-error.log*
3535
*.tsbuildinfo
3636
next-env.d.ts
3737

38-
# contentlayer
39-
.contentlayer
38+
.env
4039

41-
.env
40+
# velite generated files
41+
.velite
42+
public/static

.vscode/settings.json

+4-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
44
["cn\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
55
],
6-
"editor.defaultFormatter": "esbenp.prettier-vscode",
6+
"editor.defaultFormatter": "biomejs.biome",
7+
"editor.codeActionsOnSave": {
8+
"source.organizeImports.biome": "explicit"
9+
},
710
"editor.formatOnSave": true,
811
"typescript.preferences.autoImportFileExcludePatterns": [
912
"@radix-ui/*",
@@ -13,8 +16,4 @@
1316
"editor.quickSuggestions": {
1417
"strings": true
1518
},
16-
"github.copilot.enable": {
17-
"*": true,
18-
"markdown": true
19-
}
2019
}

app/blog/[slug]/components/mdx.tsx

+21-16
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,32 @@
11
import { cn } from "@/components/ui/utils";
2-
import { useMDXComponent } from "next-contentlayer/hooks";
2+
import * as runtime from "react/jsx-runtime";
33

44
interface Props {
5-
code: string;
5+
code: string;
66
}
77

88
const components = {
9-
a: ({ className, ...props }: React.HTMLAttributes<HTMLAnchorElement>) => (
10-
<a
11-
target="_blank"
12-
rel="noopener"
13-
{...props}
14-
className={cn([className, "text-[#ff8b9e] dark:text-[#feb3a6]"])}
15-
/>
16-
),
9+
a: ({ className, ...props }: React.HTMLAttributes<HTMLAnchorElement>) => (
10+
<a
11+
target="_blank"
12+
rel="noopener"
13+
{...props}
14+
className={cn([className, "text-primary"])}
15+
/>
16+
),
17+
};
18+
19+
const useMDXComponent = (code: string) => {
20+
const fn = new Function(code);
21+
return fn({ ...runtime }).default;
1722
};
1823

1924
export function Mdx({ code }: Props) {
20-
const Component = useMDXComponent(code);
25+
const Component = useMDXComponent(code);
2126

22-
return (
23-
<div className="prose prose-slate dark:prose-invert flex-1">
24-
<Component components={components} />
25-
</div>
26-
);
27+
return (
28+
<div className="prose prose-slate dark:prose-invert flex-1">
29+
<Component components={components} />
30+
</div>
31+
);
2732
}
+10-10
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import Link from "next/link";
22

33
export default function Supplement() {
4-
return (
5-
<div className="p-2 border-l-4 border-[#AFF4C6]">
6-
<Link
7-
href="/guestbook"
8-
className="font-bold underline underline-offset-4 text-[#AFF4C6]"
9-
>
10-
🍵 방명록에 한 줄 남기고 가기 🍵
11-
</Link>
12-
</div>
13-
);
4+
return (
5+
<div className="px-3 py-2 bg-primary w-fit">
6+
<Link
7+
href="/guestbook"
8+
className="font-medium text-sm text-white"
9+
>
10+
방명록에 한 줄 남기고 가기
11+
</Link>
12+
</div>
13+
);
1414
}

app/blog/[slug]/layout.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import PageLayout from "@/components/page-layout";
22

33
export default function Layout({ children }: { children: React.ReactNode }) {
4-
return <PageLayout>{children}</PageLayout>;
4+
return <PageLayout>{children}</PageLayout>;
55
}

app/blog/[slug]/page.tsx

+38-42
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,54 @@
1-
import { notFound } from "next/navigation";
2-
import { allBlogs } from "contentlayer/generated";
1+
import metadata from "@/util/metadata";
32
import type { Metadata } from "next";
3+
import { notFound } from "next/navigation";
4+
import { blogPosts } from "#site/content";
45
import { Mdx } from "./components/mdx";
5-
import metadata from "@/util/metadata";
66
import Supplement from "./components/supplement";
77

88
interface Props {
9-
params: {
10-
slug: string;
11-
};
9+
params: {
10+
slug: string;
11+
};
1212
}
1313

14-
export default async function DocPage({ params }: Props) {
15-
const post = await getDocFromParams({ params });
16-
17-
if (!post) {
18-
notFound();
19-
}
20-
21-
return (
22-
<div className="flex flex-col mt-5 gap-1">
23-
<h1 className="break-all text-3xl font-black bg-gradient-to-r from-slate-600 via-slate-300 to-slate-700 inline-block text-transparent bg-clip-text">
24-
{post.title}
25-
</h1>
26-
<time className="text-gray-500 text-sm mt-2 ml-auto">{post.date}</time>
27-
<Mdx code={post.body.code} />
28-
<Supplement />
29-
</div>
30-
);
14+
export default function DocPage({ params }: Props) {
15+
const post = getPageBySlug(params.slug);
16+
17+
if (!post) {
18+
notFound();
19+
}
20+
21+
return (
22+
<div className="flex flex-col mt-5 gap-2">
23+
<h1 className="text-5xl font-black text-primary break-keep">{post.title}</h1>
24+
<time className="text-primary font-medium text-sm mt-2 mb-10 ml-auto">{post.date}</time>
25+
<Mdx code={post.body} />
26+
<Supplement />
27+
</div>
28+
);
3129
}
3230

33-
async function getDocFromParams({ params }: Props) {
34-
const doc = allBlogs.find((doc) => doc.slug === params.slug);
35-
36-
return doc ?? null;
31+
function getPageBySlug(slug: string) {
32+
return blogPosts.find((page) => page.slug === slug);
3733
}
3834

3935
export async function generateStaticParams() {
40-
return allBlogs.map((doc) => ({
41-
slug: doc.slug,
42-
}));
36+
return blogPosts.map((doc) => ({
37+
slug: doc.slug,
38+
}));
4339
}
4440

4541
export async function generateMetadata({ params }: Props): Promise<Metadata> {
46-
const doc = await getDocFromParams({ params });
47-
48-
if (!doc) {
49-
return {};
50-
}
51-
52-
return metadata({
53-
title: doc.title,
54-
description: doc.description,
55-
path: `/blog/${doc.slug}`,
56-
image: `/${doc.thumbnailUrl}`,
57-
});
42+
const doc = getPageBySlug(params.slug);
43+
44+
if (!doc) {
45+
return {};
46+
}
47+
48+
return metadata({
49+
title: doc.title,
50+
description: doc.description,
51+
path: doc.permalink,
52+
image: `${doc.thumbnailUrl}`,
53+
});
5854
}

app/blog/page.tsx

+34-32
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,40 @@
11
import PageLayout from "@/components/page-layout";
2-
import { allBlogs } from "contentlayer/generated";
32
import Image from "next/image";
43
import Link from "next/link";
4+
import { blogPosts } from "#site/content";
55

66
export default function BlogPage() {
7-
return (
8-
<PageLayout
9-
title="Blog"
10-
description="공유하고 싶거나 다시 보고 싶은 기술들을 정리합니다."
11-
>
12-
{allBlogs
13-
.sort((a, b) => Number(new Date(b.date)) - Number(new Date(a.date)))
14-
.map((blog) => (
15-
<Link
16-
href={`/blog/${blog.slug}`}
17-
key={blog.slug}
18-
className="flex py-5 items-start justify-between gap-2"
19-
>
20-
<div className="flex flex-col gap-1 flex-1">
21-
<span className="font-bold text-lg break-all line-clamp-2">
22-
{blog.title}
23-
</span>
24-
<span className="break-all">{blog.description}</span>
25-
<time className="text-xs text-gray-500 mt-1">{blog.date}</time>
26-
</div>
27-
<Image
28-
width={150}
29-
height={150}
30-
src={blog.thumbnailUrl}
31-
alt={blog.title}
32-
className="object-cover w-32 h-24 rounded"
33-
/>
34-
</Link>
35-
))}
36-
</PageLayout>
37-
);
7+
return (
8+
<PageLayout
9+
title="Blog"
10+
description="공유하고 싶거나 다시 보고 싶은 기술들을 정리합니다."
11+
>
12+
<div className="flex flex-col">
13+
{blogPosts
14+
.sort((a, b) => Number(new Date(b.date)) - Number(new Date(a.date)))
15+
.map((blog) => (
16+
<Link
17+
href={`${blog.permalink}`}
18+
key={blog.slug}
19+
className="flex py-5 items-center justify-between gap-2 border-b last:border-none"
20+
>
21+
<div className="flex flex-col gap-1 flex-1">
22+
<span className="font-semibold text-lg break-all line-clamp-2 text-primary">
23+
{blog.title}
24+
</span>
25+
<span className="break-all">{blog.description}</span>
26+
<time className="text-xs mt-1">{blog.date}</time>
27+
</div>
28+
<Image
29+
width={150}
30+
height={150}
31+
src={blog.thumbnailUrl}
32+
alt={blog.title}
33+
className="object-cover w-28 h-28"
34+
/>
35+
</Link>
36+
))}
37+
</div>
38+
</PageLayout>
39+
);
3840
}

app/components/banner.tsx

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
export default function HomeBanner() {
2-
return (
3-
<div className="flex flex-col gap-2 h-[300px] rounded-lg p-6 bg-no-repeat bg-center bg-[url('/home/dark.gif')]">
4-
<h1 className="text-xl font-black text-white">Miryang Jung</h1>
5-
<span className="text-white">
6-
Lazy Frontend Engineer who likes to travel
7-
</span>
8-
</div>
9-
);
2+
return (
3+
<div className="flex flex-col gap-2 h-[300px] rounded-lg p-6 bg-no-repeat bg-center bg-[url('/home/dark.gif')]">
4+
<h1 className="text-xl font-black text-white">Miryang Jung</h1>
5+
<span className="text-white">
6+
Lazy Frontend Engineer who likes to travel
7+
</span>
8+
</div>
9+
);
1010
}

app/components/recent-posts.tsx

+25-20
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
1-
import { allBlogs } from "@/.contentlayer/generated";
21
import Link from "next/link";
2+
import { blogPosts } from "#site/content";
33

44
export default function RecentPosts() {
5-
return (
6-
<>
7-
<h2 className="mt-5 text-2xl font-bold">최근 게시물</h2>
8-
{allBlogs
9-
.sort((a, b) => Number(new Date(b.date)) - Number(new Date(a.date)))
10-
.slice(0, 5)
11-
.map((blog) => (
12-
<Link
13-
href={`/blog/${blog.slug}`}
14-
key={blog.slug}
15-
className="flex flex-col gap-1 flex-1"
16-
>
17-
<span className="font-bold text-lg break-keep"> {blog.title}</span>
18-
<span>{blog.description}</span>
19-
<time className="text-xs text-gray-500 mt-1">{blog.date}</time>
20-
</Link>
21-
))}
22-
</>
23-
);
5+
return (
6+
<>
7+
<h2 className="mt-5 text-5xl font-medium text-primary">
8+
Recent <span className="font-black">Posts</span>
9+
</h2>
10+
<div className="flex flex-col">
11+
{blogPosts
12+
.sort((a, b) => Number(new Date(b.date)) - Number(new Date(a.date)))
13+
.slice(0, 5)
14+
.map((blog) => (
15+
<Link
16+
href={`${blog.permalink}`}
17+
key={blog.slug}
18+
className="flex flex-col gap-1 flex-1 border-b last:border-none py-5"
19+
>
20+
<span className="font-bold text-lg break-keep text-primary">
21+
{blog.title}
22+
</span>
23+
<span className="text-primary">{blog.description}</span>
24+
</Link>
25+
))}
26+
</div>
27+
</>
28+
);
2429
}

app/components/theme-provider.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
"use client";
22

3-
import * as React from "react";
43
import { ThemeProvider as NextThemesProvider } from "next-themes";
5-
import { type ThemeProviderProps } from "next-themes/dist/types";
4+
import type { ThemeProviderProps } from "next-themes/dist/types";
65

76
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8-
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
7+
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
98
}
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
"use server";
22

33
import supabase from "@/util/supabase";
4-
import { GuestbookFormValues } from "../components/form";
54
import { revalidatePath } from "next/cache";
5+
import type { GuestbookFormValues } from "../components/form";
66

77
export default async function insertGuestbookAction(data: GuestbookFormValues) {
8-
// !TODO 에러처리
9-
await supabase.from("guestbook").insert({
10-
...data,
11-
created_at: new Date().toISOString(),
12-
});
8+
// !TODO 에러처리
9+
await supabase.from("guestbook").insert({
10+
...data,
11+
created_at: new Date().toISOString(),
12+
});
1313

14-
revalidatePath("/guestbook");
15-
return true;
14+
revalidatePath("/guestbook");
15+
return true;
1616
}

0 commit comments

Comments
 (0)