---
title: "Markdownでブログを書けるようにした"
date: "2024-12-05"
tags:
  - "Tech"
  - "Next.js"
---

## 経緯

私、asutoは、人に見せられるような個人サイトを作りたいなと思ってB2の春頃である2023年3月3日にGoogle Domainsでこの`asuto.dev`を購入し、既に怠惰ではあるがそこから少し経った2023年6月10日には少なくとも最初のページ(個人サイト要素ゼロの`create-next-app`そのままの天然物)をVercelにデプロイした。

それからさらに放置されて2023年9月22日頃には`/about`と`/contacts`が生えて、ようやく個人サイトっぽいものが出来上がった。ところがその時点で急速にやる気がなくなってしまい、細かい更新はあれども、それから一年以上手をつけていない状態だった。

そして、2024年12月4日にTLに[個人サイトリプレイスしている人](https://x.com/chururi_)

https://x.com/chururi_/status/1864122435857158212

が流れてきて「俺もやるかぁ〜〜」となんか急にやる気が出てきたので、やった。

## やったこと

### 主なデザインの改修

流石に`create-next-app`を何も変えていないスタイルは恥ずかしい気持ちがあったので、最初にこれをやった。

適当にfigmaでフレームを組んでこんな感じにしようと考えてみて、結局React Componentに落とし込む時になんか色々変わったが、何も考えずやるよりかはマシだったと思う。

### MDXの導入

デザインをある程度改修し終わったときに、次はブログを書きたい(書くとは言っていない)と思った。

そこで、まず`@next/mdx`を導入して、`/about`, `/contacts`に適用したが、ページごとにいちいちフォルダを生やしてそのなかに`page.mdx`で書いていく感じだったため、今あるページを`.tsx`から`.mdx`にリプレイスする分には良いが、今後ブログをこの形で生やすことを考えると渋かった。
その後、後述の`react-markdown`を用いたものと共存させたときに、結局別々の二箇所でスタイルを当ててデザインの管理を行わなければならないという不便さに気づいたので、結局`@next/mdx`を廃止して`/about`, `/contacts`も`react-markdown`を用いて書き直すことにした。

### `gray-matter`と`react-markdown`による、メタデータを利用したMarkdown to HTMLの導入

結局、`MDX`でブログを書くことは諦めて、`gray-matter`と`react-markdown`を導入した。

#### `gray-matter`

Markdownの先頭に記述された記事のタイトルや日時などのメタデータを解析するライブラリ

#### `react-markdown`

後述する`remark`や`rehype`を使って、MarkdownをReactコンポーネントとしてレンダリングするライブラリ

#### `remark`

Markdownの構造を解析して`mdast`という抽象構文木(AST)を生成するライブラリ。この`mdast`を直接HTMLに変換する`remark-html`というプラグインや、この`mdast`をHTMLのASTである`hast`に変換して、さらにそれを生のHTMLに変換する`rehype`という別のライブラリと合わせて使ったりする

当初`react-markdown`ではなく直接`remark`を導入することも考えたが、`remark`と`remark-html`ではスタイルの当てられていない素のHTMLが生成されてしまい、`remark`と`rehype`を使えばスタイルは当てれるがやはりHTMLに変換されてしまい、
今後扱う時にReactコンポーネントでないと不便になるかもしれないと思ったため、最終的に`react-markdown`を採用した。このページもこの仕組みでレンダリングされている。

以下のようにして、`generateStaticParams`を用いて任意のディレクトリにあるファイル(この例では`src/blog/*.md`)を読んで、URLを任意の文字列(この例ではファイル名から`.md`を抜いたもの)にマッピングすることができた。

```typescript:src/app/blogs/[slug]/page.tsx
export function generateStaticParams() {
  const blogDir = path.join(process.cwd(), "src/blogs");
  const filenames = fs.readdirSync(blogDir).filter((file) => file.endsWith(".md"));

  return filenames.map((filename) => ({
    slug: filename.replace(/\.md$/, ""),
  }));
}

function getBlogData(slug: string): BlogPostProps {
  const filePath = path.join(process.cwd(), `src/blogs/${slug}.md`);
  const fileContent = fs.readFileSync(filePath, "utf8");

  const { content, data } = matter(fileContent) as {
    content: string;
    data: Partial<BlogMetadata>;
  };

  // 略

  return {
    content,
    metadata,
  }
}

export default async function BlogPostPage({ params }: BlogPostPageProps) {
  const { slug } = await params;
  const { content, metadata } = getBlogData(slug);

  return (
    // 略
      <MarkdownRenderer content={content} />
    // 略
  );
}
```

これで読んだファイルの中身を`gray-matter`に食わせてメタデータを取得し、内容の部分を`react-markdown`でレンダリングして表示している。

## 今後やりたいこと

> とにかくやれ!!
> (出典: とにかくやれ!! 【仕事の姿勢】- 桜井政博のゲーム作るには)
> https://youtu.be/JV3KOJ_Z4Vs?si=q7ZoIKkmtjlLdIyM&t=35

はい、（健康に無理のない範囲で）やります…

### 変なページを生やす

みんな`/575`とか`/tanka`とかやったり、写真上げたりしてるのいいなと思ったので、俺もなんかやりたい。

### 手を動かしたことをブログとして書く

ようやくこれでブログを書けるようになって、それだけで満足してしまいそうになるが、何はともあれ記事の本数は増やしたいよね。手を動かしつついい感じのタイミングでここにダンプするようにしたい。

### Markdownの解析・レンダリング方法の改善

今回は`react-markdown`を用いて、MarkdownをReactコンポーネントにしているが、ここでは以下のようにして、HTML要素ごとにスタイルを当てている。

```typescript:src/components/MarkdownRenderer.tsx
<ReactMarkdown
  remarkPlugins={[remarkGfm]}
  rehypePlugins={[rehypeSlug, rehypeAutolinkHeadings]}
  components={{
    h1: ({ ...props }) => (
      <h1 className="text-3xl font-normal border-b pb-2 mb-8" {...props} />
    ),
    h2: ({ ...props }) => (
      <h2 className="text-2xl font-normal mt-6 border-b pb-1" {...props} />
    ),
    p: ({ children, ...props }) => (
      // 略
    ),
    a: ({ children, ...props }) => (
      // 略
    ),
    // 略
  }}
>
  {content}
</ReactMarkdown>
```

しかし、HTML単位でスタイルを当てるとなると、react-markdownで出力されたタグが囲われていた時にそれらすべてのスタイルが反映されることになり、不便である。

そこで、以下のようなサイトでは`remark`で`mdast`に変換したら、それを直接Reactコンポーネントに変換するプラグインを作成している。

https://blog.stin.ink/articles/replace-react-markdown-with-remark

これと同じ方法を取るかはともかくとして、レンダリング方法に改善の余地があり、このような選択肢があることがわかった。

#### 追記

後日、本ページのようなOGPを表示できるようにしていたら、上のサイトで主張されているようなHTMLのセマンティックで処理する辛さがよくわかった。

というのも、MarkdownからOGPを表示するURLを抽出したいときに、文章の途中で`[適当なリンク](https://example.com)`という形でリンクを貼っていたときと1行全部がURLのときはHTMLにするとどちらも`a`タグであり、これを区別するような処理をすべての`a`タグについて挟まなければいけないという意味で冗長であり、HTMLではなくMarkdownのセマンティックで場合分けしたいという欲求が強い。

また、文章の段落中にあるリンクを処理したいときに、`p`の中で`a`が使われている場合この処理の結果`p`の中に`div`が入ってしまってHTMLのセマンティックに違反してしまい、エラーが出る。ここで`p`をすべて`div`に置き換えるなどすれば見た目を維持しながらエラーは解決できたが、HTMLのセマンティックを崩してまでこのようなことをするのは心が痛い。というわけでいずれ`mdast`->`React Component`なプラグインを書こうと思った。

---

## 修正履歴

### 2024-12-05

- 初稿を公開。

### 2024-12-06

- 修正履歴セクションと`@next/mdx`を廃止したことへの言及を追加。
- 文章の調整

### 2024-12-07

- OGPカードを追加
- レンダリング方法の部分に追記
