Markdown執筆機能の改善 パッチ2026年5-6月

rikuo kamadarikuo kamada

はじめに

自作ブログでは、記事本文をMarkdownで書けるようにしています。

最初は、Markdownを保存して表示できれば十分だと考えていたのですが、
実際に記事を書いたり、画像を挿入したり、プレビューを確認したりしていると、単にMarkdownを表示するだけでは足りない部分が見えてきました。

今回の記事では、ブログのMarkdown記入・表示機能をどのように改善したのかを整理します。


改善前の構成

Markdown本文の保存場所

  • 記事本文はCloudflare R2に .md ファイルとして保存
  • アプリ側ではR2からMarkdown文字列を取得
  • 表示には react-markdown を使用
  • GFM対応には remark-gfm を使用
  • 管理画面のプレビューでは rehype-sanitize を使用

問題点

  • 記事ページ、worksページ、管理画面プレビューでMarkdown描画処理が分散していた
  • 独自の画像表示ルールやリンク処理を複数箇所で扱う必要があった
  • 入力欄が通常の textarea ベースで、Markdown編集体験としては弱かった
  • 画像挿入やプレビューとの連携を親コンポーネント側が直接扱っていた
  • 将来的に独自記法を追加しようとすると、修正範囲が広がりそうだった

:react-markdown
markdown形式のものをreactコンポーネント内にレンダリング・また表示を簡単にカスタマイズするライブラリ。

:remark-gfm
GMF(GitHub-Flavored Markdown)つまりmarkdownの雛型・土台です。
これだけでは、基本的に機能が足りないので、追加で独自のものを実装するか他のものと合わせて作ることが多いです。

:rehype-sanitize
文字通りmarkdownのサニタイズを担います。
markdownに危険なコードがあればエスケープします。


改善方針:Markdownパーサーを自作するのではなく、扱い方を整理する

今回の方針は、Markdownパーサーそのものを全て自作することではありません。

CommonMarkやGFMのような基本的な構文解析は既存ライブラリに任せて、
その代わり、ブログ独自のルールや表示処理を一箇所にまとめることを目指しました。

自作しないもの

  • Markdownの基本構文解析
  • リスト、表、引用、コードフェンスなどの低レベル処理
  • サニタイズ処理のフルスクラッチ実装

自作・整理するもの

  • 画像表示ルール
  • 内部リンク / 外部リンクの出し分け
  • URL単体行のリンクカード化
  • 画像キャプションの扱い
  • コードブロックの表示
  • 目次生成
  • OGP description用のテキスト抽出
  • React用の共通Markdownレンダラ

1. /lib/markdown にMarkdown処理を集約

まず、Markdown関連の処理を 一箇所(/lib/markdown) に集約しました。

これにより、記事ページ・worksページ・管理画面プレビューが、それぞれ個別に react-markdown を直接扱うのではなく、共通レンダラを経由してMarkdownを表示する構成にした。

主な構成

  • index.tsx

    • 公開API
  • constants.ts

    • 共通のprose class
  • types.ts

    • renderer / componentsの型
  • utils.ts

    • origin解決
    • bare URL判定
    • description抽出
    • heading id生成
  • toc.ts

    • h2 / h3から目次データを生成
  • components.tsx

    • img、a、p、h1、h2、h3、code、preの共通描画
  • renderer.tsx

    • ReactMarkdownの共通ラッパー
  • CopyCodeButton.tsx

    • コードブロック用のコピーボタン

この構成にしたことで、Markdownの表示ルールを変更したい場合に、記事ページ・worksページ・プレビュー画面を個別に修正する必要がなくなりました。


2. 画像表示ルールの整理

ブログ記事では、画像の扱いがかなり重要になっています。

今回の改善では、Markdown内の画像を単純な <img> として表示するだけでなく、Next.js側の画像表示 < Image > に差し替えるようにしました。

さらに、独自記法として次のような幅指定にも対応しました。

md
![説明文|w=700](/media/example.webp)

このように書くことで、Markdown上で画像幅を指定で決まるようにしました。

また、画像の直下に単独の斜体テキストを置いた場合は、画像キャプションとして扱うようにしました。

md
![](/media/example.webp)
*これは画像のキャプションです*

これにより、記事本文の中で画像と説明文を自然に扱えるようになりました。


3. リンク処理の共通化

リンクについても、単純に <a> タグとして表示するだけではなく、内部リンクと外部リンクを出し分けるようにしました。

  • 内部リンク
    →アプリ内遷移として扱う

  • 外部リンク
    → 外部サイトへのリンクとして扱う

    テストリンク

  • URLだけの段落
    → リンクカードとして表示する

example.com
https://example.com/

これにより、Markdownを書く側は通常のリンク記法を書くだけで、表示側が適切に処理してくれます。


4. コードブロックの可読性の向上

技術記事ではコードブロックの見やすさも重要になると考え、
今回の改善では、fenced code blockの言語名を取得し、コードブロック上部に表示するようにした。

php
echo "hello";

表示上は、コードブロックのヘッダー左側に php のような言語名を出し、右側にコピーボタンを配置する構成にしました。

これにより、読者がコードの種類を把握しやすくなり、必要であればすぐにコピーできるようになりました。


5. 目次の自動生成

また、zenn や qiita のように記事本文の h2 / h3 をもとに、目次を生成するようにしました。

Markdown本文から見出しを抽出し、本文側の見出しにも同じIDを付与することにより、
目次をクリックすると該当箇所へ移動できるようになりました。

加えて、固定ヘッダーに見出しが隠れないように見出し側にはスクロール位置の調整も入れました。


6. 改行ルールの調整

Markdownでは通常、単なる改行がそのまま <br> にならないことがあります。
元のmarkdownでは、「文末半角の空白 *2 + 改行」にしないと<br> 判定されませんでした。

ブログ記事を書く感覚として、エディタ上で改行したら表示側でも改行されてほしい場面が多く、ここは前からかなり不便に感じていました。

そこで remark-breaks を使い、soft breakをhard breakとして扱うようにし改行がそのまま <br> として扱われるようにしました。

これにより、半角スペース2つを意識しなくても、Markdown上の改行が表示に反映されるようになりました。

github.com
https://github.com/remarkjs/remark-breaks

7. MarkdownEditorコンポーネントに集約

表示側の整理だけでなく、入力側も一部整理しました。

まず MarkdownEditor コンポーネントを新設し、親コンポーネントが直接 textarea のDOM APIを触らない構成にしました。

MarkdownEditor は次のようなAPIを持ちます。

  • value
  • onChange
  • name
  • placeholder
  • minLength
  • required
  • disabled
  • className
  • focus()
  • insertText()

最初は内部実装を textarea ベースにして、先にコンポーネント境界を作理、
その後、中身をCodeMirror実装へ差し替えました。
この段階を踏んだことで、呼び出し側の構造を大きく変えずに、エディタ部分だけを置き換えることができました。


8. CodeMirrorの導入

上の記述と一部内容が被りますが、管理画面の本文入力欄を通常の textarea からCodeMirrorに移行しました。

CodeMirrorはMarkdown編集用のUIとして使い、プレビューや公開画面の描画は既存のMarkdownレンダラ基盤で行う構成になっています。

ここで重要なのは、CodeMirrorを「表示用のMarkdownレンダラ」として使っているわけではない、という点です。

  • CodeMirror

    • Markdownを書くためのエディタ
  • react-markdownベースの共通レンダラ

    • Markdownを表示するための仕組み

この役割を分けておくことで、編集画面と公開画面の責務が混ざらないようにしています。


9. 画像挿入もCodeMirrorに対応

画像アップロード時には、生成されたMarkdownをカーソル位置へ挿入する必要があります。

CodeMirror導入後も、MarkdownEditorinsertText() 経由で本文へMarkdownを挿入できるようにしました。

これにより、CreatePost / EditPost側は、エディタの中身が textarea なのかCodeMirrorなのかを意識する必要がなくなりました。

画像挿入、プレビュー、保存、更新の既存フローも維持できています。


10. hidden textareaは残す

CodeMirrorに移行しても、フォーム送信用には hidden textarea を残しました。

これは、既存のフォーム送信の流れを大きく壊さず、CodeMirrorで編集した値を従来のフォーム処理に乗せるためです。

エディタUIはCodeMirrorに置き換えつつ、保存処理との接続部分は元の安定した形を保つようにすることにしました。


今後の課題

今回の改善で以前までの「処理がバラバラの場所に描かれ、markdownも書きにくい」状態だったものが、 Markdownの編集・表示の基盤を整理できたと思います。

今後の候補としては、主に以下です。

  • CodeMirrorの見た目調整
  • シンタックスハイライトの導入
  • 独自記法の補助入力
  • Markdown記法の補完
  • ショートカット追加
  • 将来的なライブラリ分離

特に、独自記法を増やす場合はエディタ側の見え方と公開画面での解釈がずれないようにする必要があり、そこも調整が少し大変だと思います。


おわりに

今回の改善は、単に入力欄をCodeMirrorに置き換えたというよりも、ブログ全体のMarkdown処理を整理する作業だったと言えます。

「Markdownを保存する」
「Markdownを表示する」
「Markdownをプレビューする」
「Markdownに画像やリンクやコードブロックを含める」
「Markdownを将来的に拡張できるようにする」
など、
これらを一つずつ整理したことで、記事を書く体験と記事を読む体験の両方を改善できたのではないかと思います。

今後も、Markdownを単なる本文フォーマットとしてではなく、自作ブログの中核機能として育てていくつもりです。

Author

rikuo kamadarikuo kamada

主にシステム面で学習したことをまとめています。 フロント、サーバー、インフラ、AIなど細かい分野に絞らずに広く発信していきます。