tech-book-labs
データユーティリティ · 最終検証 2026-05-26 · marked 17.0.3 · 初公開 2026-05-09

marked v17 で Markdown を HTML に変換する

marked v17 系で Markdown → HTML を最小コードでパースする方法、GFM 拡張、DOMPurify 併用のサニタイズ。Lexer → Parser → Renderer 3 段パイプラインと拡張ポイント、breaks: true で挙動が変わる罠、async: true で Promise になる API 差まで実装メモ。

marked markdown javascript

検証日: 2026-05-26

使用バージョン: marked@17.0.3(2026-05 時点で latest)

対象: ブラウザ / Node で MD → HTML 変換が必要な場面、軽量 Markdown プレビューを実装したい人

marked v17 で Markdown → HTML 変換 を最小コードで動かすパターン。GFM 拡張、サニタイゼーション(DOMPurify 併用)、カスタムレンダラ、同期 vs 非同期、を動く demo で確認します。

触って試す

セットアップ(npm 1 行 / ESM only / 型同梱)

npm i marked
# サニタイズ用に DOMPurify を一緒に
npm i dompurify
npm i -D @types/dompurify
項目
公開バージョン17.0.3
Node 必要バージョン18+(v15 系から ESM only に移行済み)
TypeScript 型本体同梱(@types/marked は不要)
バンドル30KB 前後(min+gzip)、tree-shake で renderer のみ抜き出し可
サニタイズデフォルトでサニタイズしない設計(関心の分離)。dompurify 等を併用必須
ライセンスMIT

marked v15 以降は ESM onlyrequire("marked") で読もうとすると Node でも ERR_REQUIRE_ESM が出る。CommonJS のままにしたい古いプロジェクトは v14 系で止める判断もあり(ただしセキュリティ更新は v15+ のみ)。

最小サンプル(marked v17、3 行で動かす)

marked.parse(src) で Markdown 文字列を HTML 文字列に変換するだけ。ブラウザでも Node でも同じ API:

import { marked } from "marked";

const html = marked.parse("# Hello\n\n**bold** _italic_");
// "<h1>Hello</h1>\n<p><strong>bold</strong> <em>italic</em></p>"

ブラウザでも Node でも同じ API。

marked 内部の 3 段パイプライン (Lexer → Parser → Renderer)

marked.parse() の中身は 3 段のパイプ。各段で hook できるので、カスタマイズしたい時にどこに割り込むかが見えやすい。

marked の Lexer → Parser → Renderer 3 段パイプラインと拡張ポイント (クリックで拡大)

実用例:

  • link を全部 target="_blank" rel="noopener" にしたい → renderer 上書き(後述)
  • :::note のようなカスタム記法を追加 → lexer 拡張で新トークンを定義
  • AST だけ欲しい(HTML 不要)marked.lexer(src) で tokens を直接取得

オプションで GFM テーブル / ストライクスルー / タスクリスト / autolink を有効化:

const html = marked.parse(src, { gfm: true, breaks: true });
オプション効果
gfm: trueTable、strike-[x] チェックリスト、autolink を有効化
breaks: true単純改行を <br> に変換(GitHub の挙動)
pedantic: falsetrue にすると Markdown.pl 厳密、false の方が一般的

サニタイゼーション 1 行(DOMPurify 併用)

marked.parse()HTML をそのまま透過する(<script> も含む)。ユーザー入力をパースする時は サニタイズ必須

import { marked } from "marked";
import DOMPurify from "dompurify";

const html = DOMPurify.sanitize(marked.parse(userInput) as string);
container.innerHTML = html;

カスタムレンダラ

marked.Renderer を継承して特定要素の出力 HTML を上書き。link に rel="noopener" を強制する例:

const renderer = new marked.Renderer();
renderer.link = ({ href, text }) => `<a href="${href}" rel="noopener noreferrer" target="_blank">${text} ↗</a>`;

marked.use({ renderer });

特定の要素だけ書き換えたい時に使う。リンクに rel="noopener" を強制する、画像に loading="lazy" を付けるなど。

同期 vs 非同期 — 2 つの API

デフォルトは同期で文字列を返す。async プラグイン(remote fetch する transformer 等)を使う場合だけ async: true で Promise になる:

// 同期(デフォルト、文字列を返す)
const html = marked.parse(src) as string;

// 非同期(Promise を返す、async プラグインを使う時)
const html = await marked.parse(src, { async: true });

カスタム async プラグインを使わない通常用途は同期で OK。

Shiki で 4 言語のコードハイライト後付け

marked 自体はコードハイライトしない(<pre><code class="language-ts">...</code></pre> を生成するだけ)。Shiki / Prism / highlight.js を marked.use({ async, walkTokens }) で噛ませる:

import { marked } from "marked";
import { createHighlighter } from "shiki";

const highlighter = await createHighlighter({
  themes: ["github-light", "github-dark"],
  langs: ["ts", "tsx", "bash", "json"],
});

marked.use({
  async: true,
  walkTokens: async (token) => {
    if (token.type === "code") {
      const lang = (token as any).lang || "text";
      // text トークンを HTML 文字列で上書き(renderer の code を別途定義してもよい)
      (token as any).text = highlighter.codeToHtml(token.text, {
        lang,
        themes: { light: "github-light", dark: "github-dark" },
      });
    }
  },
});

const html = await marked.parse(src);  // 非同期になる

walkTokens で個別 token を書き換える方が、renderer.code を丸ごと上書きするより副作用が少ない。Shiki を入れると bundle は +1MB 程度(言語数で増減)、SSR で highlighter を作って dist に焼く方が速い。

6 つの落とし穴と直し方

症状原因直し方
<script>alert(1)</script> がそのまま出るmarked は デフォルトで HTML を素通しDOMPurify.sanitize(marked.parse(input)) で必ずサニタイズしてから innerHTML
await marked.parse(src) の戻り値が string 型エラーasync: truePromise<string>const html = await marked.parse(src, { async: true }) で型を合わせる
段落内の改行が無視されるデフォルトは CommonMark 準拠(2 スペース + 改行で <br>)breaks: true で GitHub 風に。ただし他の MD と挙動がズレるので注意
GFM table が table にならないセル境界の **`` 周囲スペース** 不足
require("marked")ERR_REQUIRE_ESMv15+ は ESM onlyimport { marked } from "marked" に置換、または v14 系で固定
Vite で marked の側で Buffer is not definedNode API を bundler が解決optimizeDeps.include: ["marked"] を vite.config に追加、または marked/lib/marked.esm.js を直接 import

評価

観点評価コメント
学習コストAPI は parse(text, options) 中心
速度ベンチで markdown-it と僅差、十分速い
サニタイズ自前で DOMPurify 等を噛ませる
拡張renderer / extensions / hooks で柔軟
バンドルサイズ30KB 前後(min+gzip)

向く / 向かないケース

  • 向く: ブラウザ内 MD プレビュー、Node での HTML 生成、軽量 docs viewer
  • 向かない: HTML / MDX の expression が必要(MDX 自体を使う)、AST 操作が中心(remark / unified を直接使う)
  • 向かない: 高度なサニタイズ要件 → markdown-it + DOMPurify か rehype-sanitize 系

関連 Topic / 関連書籍

この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:

tech-book.net /books/9784297144944

JavaScriptによるはじめてのアルゴリズム入門 | tech-book.net

河西 朝雄

関連テーマ・学習ロードマップから次の一冊へ。

詳細を tech-book.net で見る
tech-book.net /books/9784873118086

Python と JavaScriptではじめるデータビジュアライゼーション | tech-book.net

Kyran Dale/嶋田 健志/木下 哲也

関連テーマ・学習ロードマップから次の一冊へ。

詳細を tech-book.net で見る
tech-book.net /books/9784839966645

React Native+Expoではじめるスマホアプリ開発 : JavaScriptによるアプリ構築の実際 | tech-book.net

松澤 太郎

関連テーマ・学習ロードマップから次の一冊へ。

詳細を tech-book.net で見る
tech-book.net /books/9784627857216

はじめてのWebデザイン&プログラミング : HTML、CSS、JavaScript、PHPの基本 | tech-book.net

村上 祐治

関連テーマ・学習ロードマップから次の一冊へ。

詳細を tech-book.net で見る
tech-book.net /books/9784297138714

フロントエンドの知識地図ーー 一冊でHTML/CSS/JavaScriptの開発技術が学べる本 | tech-book.net

株式会社ICS 池田 泰延/西原 翼/松本 ゆき

関連テーマ・学習ロードマップから次の一冊へ。

詳細を tech-book.net で見る