PR

【Next.jsブログ構築】コードブロックにシンタックスハイライトとコピーボタンを追加する方法

Next.jsブログのコードブロックにシンタックスハイライトとコピーボタンを追加する手順を解説するイラスト。色付きのコードブロックとコピーボタンが表示されたブログ画面を視覚化したアイキャッチ画像 VPS・RentalServer
この記事は約14分で読めます。
記事内に広告が含まれています。
スポンサーリンク

これまでの Next.js ブログサイト構築シリーズをまとめたページは以下の通りです。

第13回でデザインを整えてブログとして公開できる見た目になりましたが、
実際に記事を公開してみて気づいた問題がありました。

Obsidian で書いているときはコードブロックに色がついて見やすく表示されているのに、
Next.js ブログに公開するとコードブロックが単色のプレーンテキストになってしまっていたのです。

コピーボタンも表示されません。

技術系の記事を書くうえでコードブロックの見やすさは重要です。
この記事では react-markdown で構築した Next.js ブログに、
シンタックスハイライト(言語別の色付き表示)とコピーボタンを追加する手順を解説します。

この記事では以下の内容を解説します。

  • なぜ react-markdown ではコードブロックに色がつかないのか
  • react-syntax-highlighter を使ったシンタックスハイライトの実装手順
  • クライアントコンポーネントへの分離が必要な理由
  • コードブロックにコピーボタンを追加する手順

記事の前提条件と対象読者

この記事は、以下の環境と進捗を前提として手順を解説しています。

  • Next.js のバージョン: Next.js 16.2 系( React 19)環境の App Router を使用しています。
  • 対象読者: シリーズ第13回までを完了し、 react-markdown で記事本文を表示している方を対象としています。

なぜコードブロックに色がつかないのか

Obsidian はシンタックスハイライトが標準で組み込まれています。
しかし react-markdown はデフォルトではシンタックスハイライトの機能を持っていません。

コードブロックをただの <code> タグとして出力するだけなので、言語ごとの色付けは行われません。

色付きのコードブロックにするには、追加でシンタックスハイライト用のライブラリを導入する必要があります。

実装方針

シンタックスハイライトを実装する方法はいくつかあります。

rehype-pretty-coderehype-highlight といった rehype 系プラグインを使う方法もありますが、今回は react-syntax-highlighter を使う方法を採用しました。

理由は、現在 rehype-raw(記事内の HTML タグをそのまま描画するプラグイン)を使用しており、追加で rehype 系のプラグインを入れると競合するリスクがあるためです。

react-syntax-highlighterreact-markdowncomponents prop を使ってコードブロックの描画だけをカスタマイズする方式のため、既存の構成に影響を与えません。

components prop とは、react-markdown が各 HTML 要素を描画するときに「この要素だけ自分で作ったコンポーネントに差し替える」という設定のことです。今回はコードブロック( <code> タグ)の描画だけを差し替えます。

実装手順

作業は以下の4つのフェーズで進めます。

PHASE内容変更ファイル
0パッケージのインストールなし
1CodeBlock.tsx を新規作成する1ファイル(新規)
2page.tsx を修正する1ファイル
3ビルドして動作確認するなし

PHASE 0:パッケージをインストールする

VPS に SSH 接続した状態で、以下のコマンドを実行してください。

cd ~/example-blog
npm install react-syntax-highlighter @types/react-syntax-highlighter
  • react-syntax-highlighter :シンタックスハイライトを行うライブラリ本体です
  • @types/react-syntax-highlighter :TypeScript がこのライブラリを正しく認識するために必要な型定義ファイルです。これがないと TypeScript のエラーが発生します

インストール後に「1 moderate severity vulnerability」という警告が表示される場合があります。
これはビルドツール系のパッケージに含まれる中程度の警告で、サイト訪問者への直接的な影響はほとんどありません。
npm audit fix はパッケージ構成が予期せず変わるリスクがあるため、今回は実行しませんでした。

インストールが正しくできたか確認します。

cat package.json | grep "react-syntax-highlighter"

"react-syntax-highlighter": "15.x.x" のように表示されれば成功です。

PHASE 1:CodeBlock.tsx を新規作成する

今回のポイントとして、コピーボタンには onClick(クリックイベント)が必要です。

実は最初、CodeBlock 関数を page.tsx に直接書いてビルドしたところ、以下のエラーが出て失敗しました。

Error: Event handlers cannot be passed to Client Component props.
{onClick: function onClick, ...}
If you need interactivity, consider converting part of this to a Client Component.

「 Event handlers cannot be passed to Client Component props 」、つまり「イベントハンドラー( onClick のような処理)はクライアントコンポーネントでしか使えません」というエラーです。
このエラーを調べて初めて、onClick を使うにはクライアントコンポーネントへの分離が必要だということがわかりました。

Next.js では、ファイルはデフォルトで「サーバーコンポーネント」として動作します。
サーバーコンポーネントはサーバー上で HTML を生成するだけなので、クリックのようなブラウザ上のインタラクションを直接扱えません。

onClick を使うには「クライアントコンポーネント」として宣言する必要があります。
クライアントコンポーネントにするには、ファイルの先頭に 'use client'; という1行を追加します。

個別記事ページの page.tsx 全体をクライアントコンポーネントにしてしまうと、サーバーサイドレンダリングの恩恵が失われます。
そのため、コードブロック部分だけを別ファイル( CodeBlock.tsx )に分離して、そちらだけをクライアントコンポーネントにします。

Cursor で ~/example-blog/components/CodeBlock.tsx を新規作成し、以下のコードを貼り付けて保存してください。

'use client';
// 'use client' を先頭に書くことで、このコンポーネントがクライアントコンポーネントになる。
// クライアントコンポーネントにしないと onClick(コピーボタン)が使えない。

import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
// oneDark はシンタックスハイライトのカラーテーマ。
// ダークテーマのブログに馴染みやすい定番テーマ。

export function CodeBlock({
  className,
  children,
}: {
  className?: string;
  children?: React.ReactNode;
}) {
  // react-markdown がコードブロックを描画するとき、言語指定(```typescript など)を
  // className="language-typescript" という形で渡してくる。
  // 正規表現でその言語名を取り出す。
  const match = /language-(\w+)/.exec(className || '');
  const language = match ? match[1] : '';

  // children(コードの中身)を文字列に変換する。
  // 末尾の改行を取り除いているのは表示が崩れないようにするため。
  const codeString = String(children).replace(/\n$/, '');

  // コピーボタンをクリックしたときの処理。
  // navigator.clipboard.writeText() はブラウザ標準の API で、
  // 指定した文字列をクリップボードにコピーする。追加パッケージは不要。
  const handleCopy = () => {
    navigator.clipboard.writeText(codeString);
  };

  // 言語指定がある場合(```typescript や ```bash など)はシンタックスハイライトを適用する
  if (language) {
    return (
      <div className="relative group my-4">

        {/* コードブロックの上部バー:左に言語名、右にコピーボタン */}
        <div className="flex items-center justify-between bg-[#282c34] px-4 py-1.5 rounded-t-md border-b border-[#3a3f4a]">
          <span className="text-xs text-[#6b7280]">{language}</span>
          <button
            onClick={handleCopy}
            className="text-xs text-[#6b7280] hover:text-[#e2e4ea] transition-colors px-2 py-0.5 rounded border border-[#3a3f4a] hover:border-[#6b7280]"
          >
            コピー
          </button>
        </div>

        {/* コード本体 */}
        <SyntaxHighlighter
          style={oneDark}
          language={language}
          PreTag="div"
          customStyle={{
            margin: 0,
            borderRadius: '0 0 6px 6px',
            fontSize: '0.875rem',
          }}
        >
          {codeString}
        </SyntaxHighlighter>
      </div>
    );
  }

  // 言語指定がない場合(インラインコードなど)はスタイルだけ整えて表示する
  return (
    <code className="bg-[#2a2d38] text-[#e2e4ea] px-1.5 py-0.5 rounded text-sm font-mono">
      {children}
    </code>
  );
}

コードの重要なポイントを補足します。

PreTag="div" について

react-syntax-highlighter はデフォルトでコードを <pre> タグで囲みます。
しかし prose クラスが <pre> タグに独自のスタイルを当てているため、<div> に変更して競合を避けています。

oneDark について

ハイライトのカラーテーマです。ダークテーマのブログに馴染みやすい定番テーマです。
他のテーマに変えたい場合は import 文の oneDark を以下のように差し替えるだけです。

// 例:VS Code のダークテーマ風
import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism';

// 例:Dracula テーマ
import { dracula } from 'react-syntax-highlighter/dist/cjs/styles/prism';
Next.jsサイトに表示したコードブロックの画像。コードが色分けされてみやすくなり、右上にはコピーボタンも表示されている。

上の画像は、今回の作業後に Next.js サイトに表示したコードブロックの例です。
コードが色分けされて見やすくなり、右上にはコピーボタンも表示されています。

PHASE 2:page.tsx を修正する

~/example-blog/app/posts/[id]/page.tsx を2箇所修正します。

import 文を追加する

ファイル先頭の import 文の末尾に以下の1行を追加してください。

変更前:

import { getPostData, getSortedPostsData } from '../../../lib/posts';
import ReactMarkdown from 'react-markdown';
import rehypeRaw from 'rehype-raw';
import Image from 'next/image';
import Link from 'next/link';
import type { Metadata } from 'next';

変更後:

import { getPostData, getSortedPostsData } from '../../../lib/posts';
import ReactMarkdown from 'react-markdown';
import rehypeRaw from 'rehype-raw';
import Image from 'next/image';
import Link from 'next/link';
import type { Metadata } from 'next';
import { CodeBlock } from '../../../components/CodeBlock';

'../../../components/CodeBlock''../../../' は、app/posts/[id]/ から3階層上(プロジェクトのルート)に戻って components/CodeBlock を読み込む、という意味です。

ReactMarkdown に components を追加する

記事本文を表示している <ReactMarkdown> タグを以下のように変更してください。

変更前:

<ReactMarkdown rehypePlugins={[rehypeRaw]}>
  {postData.content}
</ReactMarkdown>

変更後:

<ReactMarkdown
  rehypePlugins={[rehypeRaw]}
  components={{
    code: ({ className, children }) => (
      <CodeBlock className={className}>
        {children}
      </CodeBlock>
    ),
  }}
>
  {postData.content}
</ReactMarkdown>

components prop を追加することで、react-markdown がコードブロックを描画するときに標準の <code> タグではなく、先ほど作成した CodeBlock コンポーネントを使うように指定しています。

code: の部分が「コードブロックの描画を差し替える」という設定です。className(言語情報)と children(コードの中身)を受け取って CodeBlock に渡しています。

PHASE 3:ビルドして動作確認する

変更が完了したらビルドします。

cd ~/example-blog
npm run build

エラーなく完了したらデプロイします。

./deploy.sh

ブラウザで個別記事ページを開き、以下の点を確認してください。

  • コードブロックに言語別の色がついているか
  • コードブロック左上に言語名( typescriptbash など)が表示されているか
  • コードブロック右上に「コピー」ボタンが表示されているか
  • 「コピー」ボタンをクリックしてコードがクリップボードにコピーされるか
Next.jsのコードブロックのイメージ画像。コードが色分けされ、それぞれのコードブロックにコピーボタンが表示されている。

上の画像は、記事の中で複数のコードブロックが表示されている例です。

変更後のフォルダ構成

今回の作業で components/CodeBlock.tsx が追加されました。

example-blog/
├── app/
│   ├── layout.tsx
│   ├── page.tsx
│   ├── page/
│   │   └── [pageNum]/
│   │       └── page.tsx
│   └── posts/
│       └── [id]/
│           └── page.tsx  ← components を追加
├── components/
│   ├── Header.tsx
│   ├── Footer.tsx
│   └── CodeBlock.tsx     ← 今回新規作成
├── lib/
│   └── posts.ts
└── posts/
    └── (Markdown 記事)

まとめ

今回の実装で、コードブロックが見やすい色付き表示になり、コピーボタンも使えるようになりました。

実装のポイントを振り返ります。

  • react-markdown はデフォルトでシンタックスハイライトを持たないため、react-syntax-highlighter を追加して components prop で差し替える
  • コピーボタンの onClick にはクライアントコンポーネントが必要なため、CodeBlock.tsx を別ファイルに分離して 'use client' を宣言する
  • react-markdowncomponents prop でコードブロックだけを差し替えているため、既存の構成( rehype-raw など)への影響がない

次回は Google アナリティクス( GA4 )と Google サーチコンソールの設定を行います。

コードブロックが色付きで表示されるようになったことで、記事の見やすさが格段に上がったと感じています。コピーボタンも、コードをそのまま使ってもらいたいときに欠かせない機能だと思っていたので、これで記事として必要な最低限の品質が整った気がしています。

コメント

タイトルとURLをコピーしました