[Nextjs] markdown
프로젝트 구성
-
articles => 마크다운 파일 폴더
-
lib/post.tsx => 마크다운 데이터 가져오는 용도
-
posts/[slug]/page.tsx => 블로그 내용 페이지
-
posts/page.tsx => 포스트 리스트 보여주는 페이지 (posts/list.tsx) 컴포넌트 이용
-
styles/mark.module.css => 마크다운용 css
├── articles │ ├── test.md │ └── test2.md ├── lib │ └── post.tsx ----------------- ├── favicon.ico ├── global.css ├── layout.tsx ├── page.tsx ├── posts │ ├── [slug] │ │ └── page.tsx │ ├── list.tsx │ └── page.tsx -------------- ├── styles │ └── mark.module.css
마크다운 파일 읽어오기
// post.tsx
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { remark } from 'remark';
import html from 'remark-html';
interface BlogPost {
id: string
title: string
date: string
summary: string
}
export const getAllPostsMetadata = () => {
const postsDirectory = path.join(process.cwd(), 'articles')
const fileNames = fs.readdirSync(postsDirectory);
const allPostsData = fileNames.map((fileName) => {
const id = fileName.replace(/\.md$/, ''); // 1️
const fullPath = path.join(postsDirectory, fileName);
const fileContents = fs.readFileSync(fullPath, 'utf8');
const matterResult = matter(fileContents);
const blogPost: BlogPost = {
id,
title: matterResult.data.title,
date: matterResult.data.date,
summary: matterResult.data.summary
};
return blogPost; //
});
return allPostsData.sort((a, b) => (a.date < b.date ? 1 : -1));
};
interface PostData {
title: string,
date: string,
content: string,
contentHTML: string
}
export const getPostData = async (fileName: string) => {
const postsDirectory = path.join(process.cwd(), 'articles')
fileName += '.md'
const fullPath = path.join(postsDirectory, fileName);
const fileContents = fs.readFileSync(fullPath, 'utf8');
const matterResult = matter(fileContents);
const processedContent = await remark()
.use(html)
.process(matterResult.content);
const contentHtml = processedContent.toString();
const blogPost: PostData = {
title: matterResult.data.title,
date: matterResult.data.date,
content: matterResult.content,
contentHTML: contentHtml
};
return blogPost;
}
리스트 뷰
// post/page.tsx
import { getAllPostsMetadata } from '../../../lib/post';
import ListItem from './list';
import '../global.css'
export default function Posts() {
const posts = getAllPostsMetadata();
return (
<main className="flex min-h-screen" >
<section className='mt-6 mx-auto max-w-2xl'>
<ul className='w-full'>
{posts.map((post) => (
<ListItem key={post.id} post={post} />
))}
</ul>
</section>
</main>
);
}
// post/list.tsx
import Link from 'next/link';
interface BlogPost{
id: string
title: string
date: string
summary: string
}
type Props = {
post: BlogPost;
};
export default function ListItem({ post }: Props) {
const { id, title, date } = post;
return (
<li className='mt-4 text-2xl dark:text-white/90' key={id}>
<Link href={`/posts/${id}`} className='hover:text-black/70 dark:hover:text-white' target='_self'>
{title}
</Link>
<br />
<p className='text-sm mt-1'>{date}</p>
</li>
);
}
마크다운 뷰
import { getPostData } from '../../../../lib/post';
import '../../global.css'
import style from '../../../../styles/mark.module.css'
import ReactMarkdown from "react-markdown";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import remarkGfm from "remark-gfm";
import React from 'react'
import { solarizedLight, dracula, dark } from 'react-syntax-highlighter/dist/cjs/styles/hljs';
import rehypeKatex from 'rehype-katex'
export default async function Home({ params }: { params: { slug: string } }) {
const postData = await getPostData(params.slug);
return (
<main className="flex min-h-screen" >
<section className='mt-6 mx-auto prose lg:prose-xl'>
<h1 >{postData.title}</h1>
<ReactMarkdown
className={style.ReactMarkdown} // import style from '../../../../styles/mark.module.css'
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeKatex]}
components={{
a({href, children}){
return (
<a href={href} target="_blank">{children}</a> // 새탭에 열기위해서 필요
)
},
code({ node, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || "");
return match ? (
<SyntaxHighlighter language={match[1]} style={dark} className="m-0">
{String(children).replace(/\n$/, "")}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
);
},
}}
>
{postData.content}
</ReactMarkdown>
</section>
</main>
);
}
customize css
폰트 사이즈등 커스텀
.ReactMarkdown {
/* global css code */
}
.ReactMarkdown ul {
/* css code for each component */
}