【Typescript】Next.js + MDXでブログ開発

2024.03.30
2024.03.30
プログラミング
TypescriptNext.jsnext-mdx-remotegray-matter

はじめに

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.

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 through getStaticProps in next.js

GitHub - hashicorp/next-mdx-remote: Load mdx content from anywhere through getStaticProps in next.js

Load mdx content from anywhere through getStaticProps in next.js - hashicorp/next-mdx-remote

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 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

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

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-remotegray-matterをインストールします。gray-matterは、MDXファイルのメタデータを取得するためのライブラリです。

1npm install next-mdx-remote gray-matter

MDXファイルの作成

サンプル記事として、contents/posts/sample-post.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)とファイルの中身を読み込む関数を作成します。

libs/post.ts
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を使ってメタデータを取得して、titlecategoryを表示しています。また、MDXRemoteコンポーネントにMDXファイルの内容を渡して表示します。

app/post/[...slug]/page.tsx
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

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.tsxremark-gfmを追加します。

app/post/[...slug]/page.tsx
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を追加します。

tailwind.config.ts
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のスタイルを適用します。

app/post/[...slug]/page.tsx
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を使ってブログを作成できるようになると思います。

参考

Support

\ この記事が役に立ったと思ったら、サポートお願いします! /

buy me a coffee
Share

Profile

author

Masa

都内のIT企業で働くエンジニア
自分が学んだことをブログでわかりやすく発信していきながらスキルアップを目指していきます!

buy me a coffee