markdown 블로그 개발하기
지금 블로그는 nextjs의 mdx지원 기능을 이용해서 구현되었고, vercel로 호스팅중 이다.
블로그 리모델링할 겸 해서 개선하고 싶은 점이 있어서 구조좀 변경 할려고 한다.
그런데 오랜만에 할려고 하니까 어떤 구조인지 기억이 안나서 포스팅을 해본다.
Netxjs로 블로그 만들기
Feature 항목
- mdx 파싱하여 html으로 변환 해서 페이지로 만들기
- sitemap3 만들기
- SEO를 위해 필요함
- RSS Feed 설정
구현 하기
- mdx 파싱해서 -> 페이지 만들기
nextjs에서 turbo pack를 사용해서 할려면은 next.config.ts에서 설정해 줘야 한다.
// next.config.ts
import createMDX from "@next/mdx";
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],
};
const withMDX = createMDX({
options: {
remarkPlugins: [
"remark-breaks",
"remark-gfm",
["remark-toc", { heading: "목차" }],
],
rehypePlugins: [
["rehype-slug"],
[
"rehype-autolink-headings",
{
behavior: "append",
},
],
["rehype-katex", { strict: true, throwOnError: true }],
"rehype-plugin-image-native-lazy-loading",
["rehype-prism-plus", { defaultLanguage: "js", ignoreMissing: true }],
],
},
});
export default withMDX(nextConfig);
mdx파일 수집해서 페이지로 만들기
// mdx파일들 읽어서 객체화하기
export function getBlogSlugs() {
const dir = path.join(process.cwd(), "src", "content", "blog");
return fs
.readdirSync(dir)
.filter((file) => path.extname(file) === ".mdx")
.map((file) => path.basename(file, path.extname(file)));
}
export async function importBlogContent(slug: string): Promise<ContentModule> {
const module = await import(`@/content/blog/${slug}.mdx`);
return {
...module,
metadata: MetadataSchema.parse(module.metadata),
};
}
export async function getAllBlogContents() {
const contents = await Promise.all(
getBlogSlugs().map(async (slug) => {
const { metadata } = await importBlogContent(slug);
return {
metadata,
slug,
};
}),
);
contents.sort((a, b) =>
b.metadata.publishedAt.localeCompare(a.metadata.publishedAt),
);
return contents.filter(({ metadata }) => !metadata.public);
}
// app/blog/[slug]/page.tsx
export const dynamicParams = false;
export function generateStaticParams() {
return getBlogSlugs().map((slug) => ({ slug }));
}
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const { default: Content, metadata } = await importBlogContent(slug);
if (metadata.public) {
return notFound(); // Render 404 page
}
return <Content />;
}
- sitemap 구성하기
// app/sitemap.ts
export const dynamic = "force-static";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getAllBlogContents();
const routes = ["", "blog"].map((route) => ({
url: `${siteMetadata.siteUrl}/${route}`,
lastModified: new Date().toISOString(),
}));
const blogRoutes = posts
.filter(({ metadata }) => !metadata.public)
.map(({ metadata, slug }) => {
const date = new Date(metadata.publishedAt);
const hours = date.getHours();
return {
url: `${siteMetadata.siteUrl}/blog/${slug}`,
lastModified: new Date(date.setHours(hours - 9)).toISOString(),
};
});
return [...routes, ...blogRoutes];
}
- RSS Feed
현재 딱히 필요하진 않아서 미구현.
마무리
오랜만에 코드를 다시 보니 당시에는 이해하고 작성했던 구조도 시간이 지나면서 많이 잊어버렸다.
이번 글은 리모델링을 시작하기 전에 현재 블로그가 어떤 방식으로 동작하는지 다시 정리해보기 위한 기록이다.
이후 리모델링은 마크다운 파싱을 조금 더 유연하게 처리하기 위해 Next.js의 Turbopack 기반 MDX 처리 대신, 빌드 타임에 remark•rehype를 직접 사용하는 방식으로 구조를 변경했다.
또한 데이터베이스 관계도와 같은 다이어그램을 표시할 수 있도록 Mermaid 지원도 추가했다.