Markdown と HTML の相互変換ベストプラクティス
Markdown は「書き方の標準」がなかった
「Markdown」という名前は John Gruber が 2004 年に作成したテキスト記法を指しますが、その仕様は曖昧でした。Gruber の Markdown.pl スクリプトが実質的な仕様でしたが、多くのエッジケースが未定義のままでした。
その結果、GitHub 向けの Markdown、Reddit の Markdown、Stack Overflow の Markdown、各ブログサービスの Markdown が微妙に異なる挙動を持つという状況が生まれました。「同じ Markdown でも環境によって表示が違う」という問題です。
CommonMark の登場と GitHub Flavored Markdown(GFM)の普及によって、状況は大きく改善されました。しかし依然として「どの Markdown を使っているか」を意識することは重要です。
CommonMark:Markdown の標準化
経緯
2012 年頃、John MacFarlane(pandoc の作者)らがエッジケースを明確に定義した Markdown 仕様の策定を始めました。2014 年に CommonMark として公開されています。
CommonMark Spec は 652 の仕様例を含み、各エッジケースでの期待動作を明示しています。対応パーサーは C(cmark)、JavaScript(marked, commonmark.js)、Python(commonmark-py)など多数あります。
CommonMark の基本構文
# 見出し 1
## 見出し 2
段落は空行で区切る。
同じ行内の改行は段落の一部となる。
**太字**、*イタリック*、~~取り消し線~~(GFM拡張)
- リスト項目
- 別の項目
- ネストしたリスト(2スペースまたはタブ)
1. 番号付きリスト
2. 2番目の項目
[リンクテキスト](https://example.com)

> 引用ブロック
`インラインコード`
\`\`\`javascript
// フェンスドコードブロック
const x = 1;
\`\`\`
CommonMark が定義する厳格なルール
元々の Markdown.pl が曖昧にしていたケースを CommonMark はすべて定義しています。代表的な例:
# リスト内のブロック要素
- アイテム1
この段落はリスト項目内の段落。
(4スペースまたはタブのインデントが必要)
- アイテム2
CommonMark では「4スペースのインデント」ではなく「継続インデント(前のリスト項目の開始位置に合わせる)」を採用しており、Markdown.pl とは異なる挙動になることがあります。
GFM:GitHub Flavored Markdown の拡張
GitHub Flavored Markdown(GFM)は CommonMark をベースにして GitHub が拡張した仕様です。GFM Spec として公開されており、現在の Markdown 利用の事実上の標準になっています。
GFM の主な拡張機能
テーブル
CommonMark には標準のテーブル構文がありません。GFM で追加されました。
| アルゴリズム | 速度 | セキュリティ |
|------------|------|------------|
| SHA-256 | 中 | 高い |
| BLAKE3 | 高 | 高い |
タスクリスト
- [x] 完了したタスク
- [ ] 未完了のタスク
- [ ] 別のタスク
自動リンク
CommonMark では <https://example.com> のように山括弧が必要ですが、GFM は https:// で始まるテキストを自動リンクにします。
https://github.com → 自動的にリンク(GFMのみ)
<https://github.com> → CommonMark でも動作
取り消し線
~~取り消し線~~ → <del>取り消し線</del>
コードブロックの言語指定
コードブロックの言語指定は CommonMark でも可能ですが、GFM の定義がより明確です。
\`\`\`python
def hello():
print("Hello, World!")
\`\`\`
Markdown → HTML 変換の仕組み
変換パイプライン
Markdown テキストを HTML に変換する処理は、一般的に次のパイプラインで行われます:
Markdown テキスト
↓ トークナイズ(字句解析)
トークンの木
↓ レンダリング
生の HTML 文字列
↓ サニタイズ(重要!)
安全な HTML
↓ DOM への挿入 / SSR
表示される HTML
サニタイズを省略すると XSS 脆弱性が発生します。
JavaScript での実装例
import { marked } from 'marked'; // CommonMark + GFM 対応
import DOMPurify from 'dompurify';
// ❌ 危険:ユーザー入力をそのまま変換して挿入
const rawHtml = marked.parse(userInput);
element.innerHTML = rawHtml; // XSS の危険
// ✅ 安全:DOMPurify でサニタイズしてから挿入
const safeHtml = DOMPurify.sanitize(marked.parse(userInput));
element.innerHTML = safeHtml;
// marked の設定例(GFM + 改行動作のカスタマイズ)
import { marked } from 'marked';
marked.setOptions({
gfm: true, // GitHub Flavored Markdown を有効化
breaks: false, // 単一改行を <br> に変換しない(CommonMark準拠)
// highlight 関数でコードブロックにシンタックスハイライト
});
const html = marked.parse('## Hello\n\n**World**');
XSS 脆弱性と対策
Markdown の HTML 変換で最も重要なセキュリティ問題は XSS(クロスサイトスクリプティング)です。
攻撃パターン
1. インライン HTML の挿入
ほとんどの Markdown パーサーはインライン HTML をそのまま通過させます。
これは通常のテキストです。
<script>alert('XSS!')</script>
<img src="x" onerror="alert('XSS!')">
<a href="javascript:alert('XSS')">クリック</a>
2. リンクの javascript: プロトコル
[クリックしてください](javascript:alert('XSS'))
このリンクは一見無害に見えますが、クリックすると JavaScript が実行されます。
3. 属性へのイベントハンドラー注入
<div onmouseover="alert('XSS')">マウスを乗せてください</div>
対策:DOMPurify
DOMPurify は WHATWG の HTML Living Standard に基づき、安全でない HTML を除去します。
import DOMPurify from 'dompurify';
// デフォルト設定(安全な HTML タグと属性を許可)
const clean = DOMPurify.sanitize(dirty);
// より厳格な設定(特定のタグのみ許可)
const clean = DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a', 'ul', 'ol', 'li', 'code', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'table', 'thead', 'tbody', 'tr', 'th', 'td'],
ALLOWED_ATTR: ['href', 'target', 'rel', 'class'],
// javascript: プロトコルのリンクを除去
ALLOW_DATA_ATTR: false,
});
// 外部リンクに rel="noopener noreferrer" を自動追加
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
if (node.tagName === 'A' && node.getAttribute('href')) {
const href = node.getAttribute('href');
if (href && !href.startsWith('#') && !href.startsWith('/')) {
node.setAttribute('target', '_blank');
node.setAttribute('rel', 'noopener noreferrer');
}
}
});
サーバーサイドでのサニタイズ
Node.js / サーバーサイドでは DOMPurify(JSDOM 必要)または sanitize-html ライブラリが使えます。
// sanitize-html(Node.js)
const sanitizeHtml = require('sanitize-html');
const { marked } = require('marked');
function markdownToSafeHtml(markdown) {
const rawHtml = marked.parse(markdown);
return sanitizeHtml(rawHtml, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['h1', 'h2', 'h3', 'img']),
allowedAttributes: {
...sanitizeHtml.defaults.allowedAttributes,
'a': ['href', 'name', 'target', 'rel'],
'img': ['src', 'alt', 'width', 'height'],
},
allowedSchemes: ['http', 'https', 'mailto'], // javascript: を除外
});
}
Markdown と HTML の相互変換を試したい場合は Markdown-HTML コンバーター を使えます。
HTML → Markdown 変換の注意点
HTML を Markdown に変換する逆方向の変換は、より難しいです。
情報損失
HTML には Markdown に対応する構文がない要素が多くあります。
| HTML | Markdown での扱い |
|---|---|
<span style="color: red"> |
スタイルは失われる |
<div class="alert"> |
クラスは失われる |
<figure><figcaption> |
単純なテキストになる |
<table colspan> |
結合セルは表現できない |
<details>/<summary> |
GFM 拡張の一部は変換可 |
turndown ライブラリ(JavaScript)
JavaScript で HTML→Markdown 変換には turndown が広く使われます。
const TurndownService = require('turndown');
const { gfm } = require('@joplin/turndown-plugin-gfm');
const turndown = new TurndownService({
headingStyle: 'atx', // # 形式(ATX スタイル)
codeBlockStyle: 'fenced', // ``` 形式
bulletListMarker: '-', // リストのマーカー
});
// GFM 拡張(テーブルなど)を有効化
turndown.use(gfm);
const markdown = turndown.turndown(`
<h1>Hello</h1>
<p>This is a <strong>test</strong>.</p>
<table>
<tr><th>Name</th><th>Age</th></tr>
<tr><td>Alice</td><td>28</td></tr>
</table>
`);
静的サイトジェネレータでの活用
Next.js での Markdown 処理
Next.js でブログやドキュメントを構築する場合、Markdown の処理は次のパターンが一般的です。
// lib/markdown.js
import { unified } from 'unified';
import remarkParse from 'remark-parse'; // Markdown パース
import remarkGfm from 'remark-gfm'; // GFM 拡張
import remarkRehype from 'remark-rehype'; // Markdown → HTML AST
import rehypeSanitize from 'rehype-sanitize'; // XSS 対策(重要)
import rehypeHighlight from 'rehype-highlight'; // シンタックスハイライト
import rehypeStringify from 'rehype-stringify'; // HTML 文字列に変換
export async function markdownToHtml(markdown) {
const result = await unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkRehype)
.use(rehypeSanitize) // sanitize は必ず入れる
.use(rehypeHighlight)
.use(rehypeStringify)
.process(markdown);
return result.toString();
}
この unified エコシステムは、Markdown の AST(抽象構文木)レベルで処理を行えるため、コードハイライトや独自の変換を柔軟に追加できます。
フロントマター(Front Matter)の処理
多くの静的サイトジェネレータでは Markdown ファイルの先頭に YAML フロントマターを記述します。
---
title: "記事タイトル"
date: "2026-04-15"
tags: ["Markdown", "HTML"]
---
# 本文
ここから記事の本文が始まります。
import matter from 'gray-matter'; // フロントマターのパース
const fileContent = fs.readFileSync('post.md', 'utf-8');
const { data: frontmatter, content: markdownBody } = matter(fileContent);
console.log(frontmatter.title); // "記事タイトル"
console.log(frontmatter.date); // Date オブジェクト
Markdown のベストプラクティス
1. 使用する Markdown の方言を明示する
プロジェクトのドキュメントに「このプロジェクトでは CommonMark + GFM を使用する」と明示しましょう。特に複数の筆者が関わる場合、方言の違いが原因のトラブルを防げます。
2. リンターを使う
markdownlint は Markdown の一貫性を保つためのリンターです。VS Code 拡張機能もあります。
// .markdownlintrc.json
{
"MD013": false, // 行の長さ制限を無効化(好みによる)
"MD033": false, // インライン HTML を許可(必要な場合)
"MD041": true // ファイルの先頭は h1 で始める
}
3. コードブロックには必ず言語を指定する
\`\`\`javascript ← 言語を指定
const x = 1;
\`\`\`
\`\`\` ← 指定なし(シンタックスハイライトが効かない)
const x = 1;
\`\`\`
4. 画像に代替テキストを書く
 ← OK
 ← NG(代替テキストなし)
代替テキストは SEO とアクセシビリティの両方に影響します。WHATWG HTML Living Standard では img 要素の alt 属性は必須(decorative images は alt="")とされています。
まとめ
| 用途 | 推奨事項 |
|---|---|
| 基本仕様 | CommonMark Spec に準拠 |
| GitHub / 一般的なサービス | GFM(CommonMark + テーブル・タスクリスト等) |
| HTML 変換 | marked / unified(rehype-sanitize 必須) |
| ユーザー入力の変換 | DOMPurify または rehype-sanitize で必ずサニタイズ |
| HTML → Markdown | turndown(情報損失を理解した上で) |
| 静的サイト | unified エコシステム(remark + rehype) |
Markdown は記述の容易さと表現力のバランスが取れたフォーマットです。CommonMark と GFM の違いを理解し、変換時のセキュリティ(特に XSS 対策)を意識することで、安全で一貫したコンテンツ処理が実現できます。
参考文献
- CommonMark Spec — John MacFarlane et al.
- GitHub Flavored Markdown Spec
- WHATWG HTML Living Standard
- DOMPurify GitHub — Mario Heiderich / Cure53
- unified / remark / rehype エコシステム
- Markdown.pl — Gruber の原著作 (2004)
