メインコンテンツへスキップ
Toolsbase Logo

Markdown と HTML の相互変換ベストプラクティス

Toolsbase編集部
MarkdownHTMLCommonMarkGFM静的サイトXSS対策

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)

![代替テキスト](image.png)

> 引用ブロック

`インラインコード`

\`\`\`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. 画像に代替テキストを書く

![ロゴ画像](logo.png)          ← OK
![](logo.png)                  ← 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 対策)を意識することで、安全で一貫したコンテンツ処理が実現できます。


参考文献