サイトアイコンmaita tomoya dev io

#29 remark完全ガイド:Next.jsでMarkdownをHTMLに変換する仕組み

Markdownで書かれたブログ記事やドキュメントをWebサイトで表示するには、HTMLに変換する必要があります。本記事では、JavaScriptで最も人気のあるMarkdownプロセッサー「remark」の仕組みと使い方を、初心者にも分かりやすく解説します。

📚 目次

Markdownの基礎知識

Markdownとは?

Markdownは、テキストファイルに簡単な記号を付けることで、文書の構造を表現できるマークアップ言語です。

基本的なMarkdown記法:

# 見出し1
## 見出し2

**太字** *斜体*

- リスト項目
- リスト項目

[リンク](https://example.com)
![画像](image.jpg)

`インラインコード`

\`\`\`javascript
// コードブロック
const hello = "world";
\`\`\`

なぜHTML変換が必要か?

  • ブラウザ表示: ブラウザはHTMLしか理解できない
  • スタイリング: CSSでデザインを適用できる
  • インタラクティブ: JavaScriptで動的な機能を追加できる

remarkとは?

remarkは、Markdownを処理するためのプラグイン可能なツールです。unifiedエコシステムの一部として、以下の特徴があります:

  • プラグインシステム:機能を自由に追加・拡張できる
  • AST(抽象構文木)ベース:Markdownを構造化データとして扱う
  • 高速処理:効率的なパース処理
  • エコシステム:豊富なプラグインが利用可能

Markdown変換の仕組み

基本的な変換フロー

Markdown → Parse → AST → Transform → AST → Stringify → HTML
  1. Parse(解析):MarkdownテキストをASTに変換
  2. Transform(変換):プラグインでASTを加工
  3. Stringify(文字列化):ASTをHTMLなどの形式に変換

AST(抽象構文木)とは?

ASTは、Markdownの構造を木構造で表現したデータです。

Markdownの例:

# Hello World

これは**太字**のテキストです。

ASTの例(簡略化):

{
  "type": "root",
  "children": [
    {
      "type": "heading",
      "depth": 1,
      "children": [
        {
          "type": "text",
          "value": "Hello World"
        }
      ]
    },
    {
      "type": "paragraph",
      "children": [
        {
          "type": "text",
          "value": "これは"
        },
        {
          "type": "strong",
          "children": [
            {
              "type": "text",
              "value": "太字"
            }
          ]
        },
        {
          "type": "text",
          "value": "のテキストです。"
        }
      ]
    }
  ]
}

基本的な使い方

1. インストール

# 基本パッケージ
npm install remark remark-html

# GitHub Flavored Markdown対応(表やチェックボックス)
npm install remark-gfm

# フロントマター対応
npm install gray-matter

2. シンプルな変換

import { remark } from 'remark';
import html from 'remark-html';

const markdown = `
# Hello World

これは**Markdown**です。

- リスト1
- リスト2
`;

async function convertMarkdown() {
  const result = await remark()
    .use(html)
    .process(markdown);
  
  console.log(result.toString());
}

convertMarkdown();

出力結果:

<h1>Hello World</h1>
<p>これは<strong>Markdown</strong>です。</p>
<ul>
<li>リスト1</li>
<li>リスト2</li>
</ul>

3. プラグインを使った拡張

import { remark } from 'remark';
import html from 'remark-html';
import remarkGfm from 'remark-gfm';

const markdown = `
# タスクリスト

- [x] 完了したタスク
- [ ] 未完了のタスク

## 表

| 項目 | 説明 |
|------|------|
| remark | Markdownプロセッサー |
| unified | テキスト処理フレームワーク |
`;

async function convertWithGfm() {
  const result = await remark()
    .use(remarkGfm)  // GitHub Flavored Markdown
    .use(html)
    .process(markdown);
  
  console.log(result.toString());
}

Next.jsでの実装例

ブログシステムの実装

// lib/posts.ts
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { remark } from 'remark';
import html from 'remark-html';
import remarkGfm from 'remark-gfm';

const postsDirectory = path.join(process.cwd(), 'posts');

export async function getPostData(slug: string) {
  const fullPath = path.join(postsDirectory, `${slug}.md`);
  const fileContents = fs.readFileSync(fullPath, 'utf8');
  
  // フロントマターを解析
  const matterResult = matter(fileContents);
  
  // MarkdownをHTMLに変換
  const processedContent = await remark()
    .use(remarkGfm)           // GFM対応
    .use(html, { 
      sanitize: false         // HTMLタグを許可
    })
    .process(matterResult.content);
  
  const contentHtml = processedContent.toString();
  
  return {
    slug,
    contentHtml,
    ...matterResult.data
  };
}

Markdownファイルの例

---
title: "初めてのremark"
date: "2025-01-10"
tags: ["JavaScript", "Markdown"]
---

# remarkを使ってみよう

remarkは**強力な**Markdownプロセッサーです。

## 特徴

- 高速処理
- プラグインシステム
- AST変換

## コード例

\`\`\`javascript
const result = await remark()
  .use(html)
  .process(markdown);
\`\`\`

## 表のサポート

| 機能 | 対応 |
|------|------|
| 基本的なMarkdown | ✅ |
| GFM | ✅ |
| カスタムプラグイン | ✅ |

主要なremarkプラグイン

1. remark-gfm

GitHub Flavored Markdownのサポート

import remarkGfm from 'remark-gfm';

// 使用例
.use(remarkGfm)

追加される機能:

  • 表(テーブル)
  • タスクリスト
  • 取り消し線
  • オートリンク
  • 脚注

2. remark-prism / remark-highlight

シンタックスハイライト

import remarkPrism from 'remark-prism';

.use(remarkPrism)

3. remark-toc

目次の自動生成

import remarkToc from 'remark-toc';

.use(remarkToc, {
  heading: '目次',
  maxDepth: 3
})

4. remark-emoji

絵文字のサポート

import remarkEmoji from 'remark-emoji';

.use(remarkEmoji)
// :smile: → 😄

5. remark-math / remark-katex

数式レンダリング

import remarkMath from 'remark-math';
import remarkKatex from 'remark-katex';

.use(remarkMath)
.use(remarkKatex)

カスタムプラグインの作成

プラグインの基本構造

function myRemarkPlugin(options = {}) {
  return (tree, file) => {
    // ASTを走査して変換
    visit(tree, 'text', (node) => {
      // テキストノードを処理
      node.value = node.value.replace(
        /\[重要\]/g,
        '⚠️ 重要'
      );
    });
  };
}

// 使用
.use(myRemarkPlugin)

実践例:外部リンクに属性を追加

import { visit } from 'unist-util-visit';

function remarkExternalLinks() {
  return (tree) => {
    visit(tree, 'link', (node) => {
      // 外部リンクかチェック
      if (node.url && node.url.startsWith('http')) {
        node.data = node.data || {};
        node.data.hProperties = {
          target: '_blank',
          rel: 'noopener noreferrer'
        };
      }
    });
  };
}

unifiedエコシステム

remarkは「unified」という大きなエコシステムの一部です。

エコシステムの構成

unified(コア)
├── remark(Markdown処理)
│   ├── remark-parse(Markdown → AST)
│   └── remark-stringify(AST → Markdown)
├── rehype(HTML処理)
│   ├── rehype-parse(HTML → AST)
│   └── rehype-stringify(AST → HTML)
└── retext(自然言語処理)
    ├── retext-english(英語解析)
    └── retext-japanese(日本語解析)

remark + rehypeの連携

import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';
import rehypeHighlight from 'rehype-highlight';

const processor = unified()
  .use(remarkParse)        // Markdown → mdast
  .use(remarkRehype)       // mdast → hast
  .use(rehypeHighlight)    // コードハイライト
  .use(rehypeStringify);   // hast → HTML

const html = await processor.process(markdown);

パフォーマンス最適化

1. プロセッサーの再利用

// ❌ 非効率:毎回新しいプロセッサーを作成
async function convertMarkdown(content) {
  return await remark()
    .use(remarkGfm)
    .use(html)
    .process(content);
}

// ✅ 効率的:プロセッサーを再利用
const processor = remark()
  .use(remarkGfm)
  .use(html);

async function convertMarkdown(content) {
  return await processor.process(content);
}

2. ビルド時の事前処理

// Next.jsのgetStaticProps
export async function getStaticProps({ params }) {
  // ビルド時にMarkdownを変換
  const postData = await getPostData(params.slug);
  
  return {
    props: {
      postData
    }
  };
}

3. キャッシング

const cache = new Map();

async function getPostWithCache(slug) {
  if (cache.has(slug)) {
    return cache.get(slug);
  }
  
  const post = await getPostData(slug);
  cache.set(slug, post);
  return post;
}

トラブルシューティング

1. 表が正しく表示されない

問題: Markdown の表が HTMLのテーブルにならない

解決方法:

// remark-gfmを追加
import remarkGfm from 'remark-gfm';

.use(remarkGfm)  // これが必要
.use(html)

2. HTMLタグが削除される

問題: MarkdownにHTMLを書いても表示されない

解決方法:

.use(html, {
  sanitize: false  // サニタイズを無効化
})

3. 改行が反映されない

問題: Markdownの改行がHTMLに反映されない

解決方法:

import remarkBreaks from 'remark-breaks';

.use(remarkBreaks)  // 改行を<br>に変換

4. 日本語の見出しでIDが生成されない

問題: 見出しの自動ID生成で日本語が無視される

解決方法:

import remarkSlug from 'remark-slug';
import { slugify } from 'transliteration';

.use(remarkSlug, {
  slugify: (text) => slugify(text)
})

実装チェックリスト

remarkを使ったMarkdown処理を実装する際のチェックリスト:

  • 必要なパッケージをインストール
    • remark
    • remark-html
    • remark-gfm(表やチェックボックスが必要な場合)
    • gray-matter(フロントマターが必要な場合)
  • プロセッサーの設定
    • 必要なプラグインを追加
    • オプションを適切に設定
  • パフォーマンス最適化
    • プロセッサーの再利用
    • ビルド時の事前処理
    • 必要に応じてキャッシング
  • エラーハンドリング
    • ファイル読み込みエラー
    • パース処理エラー
  • テスト
    • 基本的なMarkdown要素
    • GFM拡張(表、チェックボックス)
    • カスタムプラグイン

ステップバイステップチュートリアル

Step 1: 最小限の実装

// 1. 必要なパッケージをインポート
const { remark } = require('remark');
const html = require('remark-html');

// 2. Markdownを準備
const markdown = '# Hello World\n\nこれは**Markdown**です。';

// 3. 変換して表示
remark()
  .use(html)
  .process(markdown)
  .then(result => {
    console.log(result.toString());
    // 出力: <h1>Hello World</h1>\n<p>これは<strong>Markdown</strong>です。</p>
  });

Step 2: ファイルから読み込む

const fs = require('fs');
const { remark } = require('remark');
const html = require('remark-html');

// Markdownファイルを読み込む
const markdown = fs.readFileSync('article.md', 'utf8');

// HTMLに変換
async function convertToHtml() {
  const result = await remark()
    .use(html)
    .process(markdown);
  
  // HTMLファイルに保存
  fs.writeFileSync('article.html', result.toString());
  console.log('✅ 変換完了!');
}

convertToHtml();

Step 3: プラグインを追加

const { remark } = require('remark');
const html = require('remark-html');
const remarkGfm = require('remark-gfm');

const markdown = `
# TODOリスト

- [x] remarkをインストール
- [ ] プラグインを追加
- [ ] カスタマイズ

| 機能 | 状態 |
|------|------|
| 表 | ✅ |
| チェックボックス | ✅ |
`;

async function convertWithPlugins() {
  const result = await remark()
    .use(remarkGfm)  // GitHub Flavored Markdownを有効化
    .use(html)
    .process(markdown);
  
  console.log(result.toString());
}

convertWithPlugins();

パフォーマンスベンチマーク

remark vs 他のMarkdownプロセッサー

プロセッサー速度プラグインASTファイルサイズ
remark速い非常に豊富
marked最速少ない
markdown-it速い豊富
showdown普通普通

大規模ファイルの処理最適化

const { unified } = require('unified');
const remarkParse = require('remark-parse');
const remarkRehype = require('remark-rehype');
const rehypeStringify = require('rehype-stringify');
const stream = require('stream');
const { promisify } = require('util');

const pipeline = promisify(stream.pipeline);

// ストリーム処理でメモリ効率を最適化
async function processLargeFile(inputPath, outputPath) {
  const processor = unified()
    .use(remarkParse)
    .use(remarkRehype)
    .use(rehypeStringify);
  
  await pipeline(
    fs.createReadStream(inputPath),
    processor.stream(),
    fs.createWriteStream(outputPath)
  );
  
  console.log('✅ 大規模ファイルの処理完了');
}

CI/CDでの活用

GitHub Actionsでのドキュメント自動生成

name: Generate Documentation

on:
  push:
    paths:
      - 'docs/**/*.md'
      - 'README.md'

jobs:
  build-docs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Build documentation
        run: |
          npm run build:docs
          
      - name: Deploy to GitHub Pages
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./dist

MarkdownのLinting

// .remarkrc.js
module.exports = {
  plugins: [
    'remark-preset-lint-consistent',
    'remark-preset-lint-recommended',
    'remark-preset-lint-markdown-style-guide',
    ['remark-lint-list-item-indent', 'space'],
    ['remark-lint-no-heading-punctuation', ':.']
  ]
};
# Lintの実行
npx remark . --quiet --frail

MDXとの統合

Reactコンポーネントの埋め込み

// components/Counter.jsx
export function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      クリック数: {count}
    </button>
  );
}
# MDXの例

import { Counter } from './components/Counter';

これは通常のMarkdownです。

<Counter />

上にインタラクティブなカウンターが表示されます!
// MDXの処理
import { compile } from '@mdx-js/mdx';
import remarkGfm from 'remark-gfm';

const compiled = await compile(mdxContent, {
  remarkPlugins: [remarkGfm],
  rehypePlugins: []
});

よくあるエラーと対処法

エラー1: Cannot find module 'remark'

# 解決方法
npm install remark remark-html

エラー2: TypeError: remark is not a function

// ❌ 間違ったインポート
const remark = require('remark');

// ✅ 正しいインポート
const { remark } = require('remark');
// または
import { remark } from 'remark';

エラー3: 表が表示されない

// ❌ GFMプラグインがない
.use(html)

// ✅ GFMプラグインを追加
.use(remarkGfm)
.use(html)

まとめ

remarkは、Markdownを扱うJavaScriptプロジェクトにおいて非常に強力なツールです:

  1. 柔軟性:プラグインシステムで機能を自由に拡張
  2. パフォーマンス:効率的なAST処理
  3. エコシステム:豊富なプラグインと関連ツール
  4. Next.js統合:静的サイト生成に最適

基本的な使い方から始めて、必要に応じてプラグインを追加していくことで、理想的なMarkdown処理環境を構築できます。

参考リンク