[Nextjs] markdown

md

프로젝트 구성

  • 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 */ }