【Typescript】Next.js + MDXでブログ開発
はじめに
Next.jsとMarkdown(MDX)を使ってブログを作成する方法を紹介します。
本ブログもNext.jsとMDXを使って作成しています。また、本ブログを元にしたサンプルリポジトリも作成していますので、実際にどのように使っているのか参考になれば幸いです。
GitHub - monda00/next-app-router-mdx-blog: This is a sample blog application using Next.js with App Router with next-mdx-remote.
This is a sample blog application using Next.js with App Router with next-mdx-remote. - monda00/next-app-router-mdx-blog
ここでのNext.jsはApp Routerを使っていることを前提にしています。
Next.jsとMDXでブログ開発
Next.jsとMarkdown(MDX)を使ってブログを作成する場合、MarkdownをHTMLに変換するライブラリが必要になります。
next-mdx-remote
next-mdx-remote
は、MDXファイルを読み込み、HTMLに変換してくれるライブラリです。
GitHub - hashicorp/next-mdx-remote: Load MDX content from anywhere
Load MDX content from anywhere. Contribute to hashicorp/next-mdx-remote development by creating an account on GitHub.
Next.jsでは、@next/mdx
というパッケージを用意していますが、このパッケージを使う場合、MDXファイルをapp/
ディレクトリに配置する必要があります。
1.
2├── app
3│ └── my-mdx-page
4│ └── page.mdx
5└── package.json
ブログの記事数が増えてくるとapp/
ディレクトリが肥大化してしまうため、MDXファイルを他のディレクトリに配置できるnext-mdx-remote
を使いたいと思います。
contentlayerについて
Next.jsとMarkdownでブログを作成する場合、contentlayer
も人気のライブラリです。
Contentlayer makes content easy for developers
Contentlayer is a content SDK that validates and transforms your content into type-safe JSON data you can easily import into your application.
ただ、2024/03現在、contentlayer
はメンテナンスされていないようなので、今回は使うのを見送りました。もう少しでメンテナンスされるようになるかもしれないみたいなIssueもあったので、メンテナンスされるようになったら使ってみようかと思います。
State of the project · Issue #429 · contentlayerdev/contentlayer
Hello Contentlayer community, I wanted to share an update regarding the ongoing maintenance of Contentlayer. I am committed to the project and will continue to work on it, aiming for monthly releas...
Proposal to become a maintainer for Contentlayer · Issue #651 · contentlayerdev/contentlayer
Hi! I've spoken with schickling late last year about continuing to support Contentlayer. Initially I suggested forking the project and there was some interest amongst the community on Discord. Schi...
Next.jsアプリの作成
まずは、Next.jsアプリを作成します。
1npx create-next-app@latest
next-mdx-remoteでMDXファイルを読み込む
next-mdx-remote
を使ってMDXファイルを読み込んでいきます。
ライブラリのインストール
まずは、next-mdx-remote
とgray-matter
をインストールします。gray-matter
は、MDXファイルのメタデータを取得するためのライブラリです。
1npm install next-mdx-remote gray-matter
MDXファイルの作成
サンプル記事として、contents/posts/sample-post.mdx
を作成します。上部にメタデータを記述し、タイトルとカテゴリを設定しています。
1---
2title: Sample Post
3category: nextjs
4---
5
6## Paragraph
7
8This is a paragraph.
9
10## List
11
12- item 1
13- item 2
14- item 3
15
16## Table
17
18| Name | Age |
19| ----- | --- |
20| Alice | 20 |
21| Bob | 25 |
22| Carol | 30 |
23
24## Quote
25
26> This is a quote.
27
28## Task List
29
30- [x] task 1
31- [ ] task 2
32- [ ] task 3
MDXを読み込む関数の作成
libs/post.ts
に、MDXファイルのファイル名(slug)とファイルの中身を読み込む関数を作成します。
1import { readFileSync, readdirSync } from "fs";
2import path from "path";
3import matter from "gray-matter";
4
5// MDXファイルのディレクトリ
6const POSTS_PATH = path.join(process.cwd(), "contents/posts");
7
8// ファイル名(slug)の一覧を取得
9export function GetAllPostSlugs() {
10 const postFilePaths = readdirSync(POSTS_PATH).filter((path) =>
11 /\.mdx?$/.test(path)
12 );
13 return postFilePaths.map((path) => {
14 const slug = path.replace(/\.mdx?$/, "");
15 return slug;
16 });
17}
18
19// slugからファイルの中身を取得
20export function GetPostBySlug(slug: string) {
21 const markdown = readFileSync(`contents/posts/${slug}.mdx`, "utf8");
22
23 const { content, data } = matter(markdown);
24 return {
25 content,
26 data,
27 };
28}
MDXファイルを表示するページの作成
記事を表示するページapp/post/[...slug]/page.tsx
を作成します。
MDXを読み込み、gray-matter
を使ってメタデータを取得して、title
とcategory
を表示しています。また、MDXRemote
コンポーネントにMDXファイルの内容を渡して表示します。
1import { GetAllPostSlugs, GetPostBySlug } from "@/libs/post";
2import { MDXRemote } from "next-mdx-remote/rsc";
3
4interface PostPageProps {
5 params: {
6 slug: string;
7 };
8}
9
10export async function generateStaticParams() {
11 const slugs = GetAllPostSlugs();
12 return slugs.map((slug) => ({ params: { slug } }));
13}
14
15export default async function PostPage({ params }: PostPageProps) {
16 const { content, data } = GetPostBySlug(params.slug);
17
18 return (
19 <div>
20 <h1>{data.title}</h1>
21 <p>{data.category}</p>
22 <div>
23 <MDXRemote source={content} />
24 </div>
25 </div>
26 );
27}
実行して確認
npm run dev
でアプリを起動して、http://localhost:3000/post/sample-post
にアクセスして記事が表示されることを確認します。
1npm run dev
見た目はあれですが、とりあえずMDXファイルを読み込んでHTMLに変換して表示できるようになりました。
Github Flavored Markdownでスタイリング
今のままだと、Github Flavored Markdown(GFM)の記法が正しくHTMLに変換されません。GFMも正しくHTMLに変換されるように、remark-gfm
をインストールします。
しかし、next-mdx-remote
のバージョン4.4.1では、remark-gfm
のバージョン4.0.0を使うと下記のようなエラーが発生します。
1Cannot set properties of undefined (setting 'inTable')
そのため、remark-gfm
のバージョンを3.0.1に指定してインストールします。
1npm install [email protected]
next-mdx-remote
のバージョン5からは、remark-gfm
のバージョン4.0.0を使うことができるようになるみたいです。
Breaks with latest version of remark-gfm; unhelpful error message · Issue #403 · hashicorp/next-mdx-remote
I'm not really sure where to file this, but: I just started using next-mdx-remote, and everything was working fine until I added the remark-gfm plugin. This is the entire error I was getting, with ...
app/post/[...slug]/page.tsx
にremark-gfm
を追加します。
1import { GetAllPostSlugs, GetPostBySlug } from "@/libs/post";
2import { MDXRemote } from "next-mdx-remote/rsc";
3import remarkGfm from "remark-gfm";
4
5interface PostPageProps {
6 params: {
7 slug: string;
8 };
9}
10
11export async function generateStaticParams() {
12 const slugs = GetAllPostSlugs();
13 return slugs.map((slug) => ({ params: { slug } }));
14}
15
16export default async function PostPage({ params }: PostPageProps) {
17 const options = {
18 mdxOptions: {
19 remarkPlugins: [remarkGfm], // remark-gfmを追加
20 },
21 };
22 const { content, data } = GetPostBySlug(params.slug);
23
24 return (
25 <div>
26 <h1>{data.title}</h1>
27 <p>{data.category}</p>
28 <div>
29 <MDXRemote source={content} options={options} />
30 </div>
31 </div>
32 );
33}
これでGFMの記法が正しくHTMLに変換されるようになりました。
Tailwind CSSでスタイリング
最後に、Tailwind CSSのTypographyプラグインを使って少しだけスタイリングをしてみます。
まずは、@tailwindcss/typography
をインストールします。
1npm install -D @tailwindcss/typography
tailwind.config.ts
に@tailwindcss/typography
を追加します。
1import type { Config } from "tailwindcss";
2
3const config: Config = {
4 content: [
5 "./pages/**/*.{js,ts,jsx,tsx,mdx}",
6 "./components/**/*.{js,ts,jsx,tsx,mdx}",
7 "./app/**/*.{js,ts,jsx,tsx,mdx}",
8 ],
9 theme: {
10 extend: {
11 backgroundImage: {
12 "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
13 "gradient-conic":
14 "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
15 },
16 },
17 },
18 plugins: [require("@tailwindcss/typography")], // 追加
19};
20export default config;
最終的にapp/post/[...slug]/page.tsx
、下記のようにprose
クラスを追加して、Tailwind CSS Typographyのスタイルを適用します。
1import { GetAllPostSlugs, GetPostBySlug } from "@/libs/post";
2import { MDXRemote } from "next-mdx-remote/rsc";
3import remarkGfm from "remark-gfm";
4
5interface PostPageProps {
6 params: {
7 slug: string;
8 };
9}
10
11export async function generateStaticParams() {
12 const slugs = GetAllPostSlugs();
13 return slugs.map((slug) => ({ params: { slug } }));
14}
15
16export default async function PostPage({ params }: PostPageProps) {
17 const options = {
18 mdxOptions: {
19 remarkPlugins: [remarkGfm],
20 },
21 };
22 const { content, data } = GetPostBySlug(params.slug);
23
24 return (
25 <div>
26 <h1>{data.title}</h1>
27 <p>{data.category}</p>
28 <div className="prose">
29 <MDXRemote source={content} options={options} />
30 </div>
31 </div>
32 );
33}
Tailwind CSS Typographyのスタイルが適用されました。(global.css
にある背景色のスタイルは削除しています)
これで、ホームや記事一覧、スタイルなどを追加していけば、Next.jsとMDXを使ってブログを作成できるようになると思います。
参考
- Configuring: MDX | Next.js
- hashicorp/next-mdx-remote: Load mdx content from anywhere through getStaticProps in next.js
- tailwindlabs/tailwindcss-typography: Beautiful typographic defaults for HTML you don't control.
- jonschlinkert/gray-matter: Smarter YAML front matter parser, used by metalsmith, Gatsby, Netlify, Assemble, mapbox-gl, phenomic, vuejs vitepress, TinaCMS, Shopify Polaris, Ant Design, Astro, hashicorp, garden, slidev, saber, sourcegraph, and many others. Simple to use, and battle tested. Parses YAML by default but can also parse JSON Front Matter, Coffee Front Matter, TOML Front Matter, and has support for custom parsers. Please follow gray-matter's author: https://github.com/jonschlinkert