コードブロックにコピーボタンを付ける―Clipboard APIとフォールバック対応
Astroの記事本文へコードブロックごとのコピー操作を追加した方法を記録します。Clipboard API、textareaフォールバック、複数コードブロック、アクセシビリティ対応を確認します。

技術記事にコードを載せていると、
「このコードをそのまま試したいのに、ドラッグしてコピーするのが面倒」
「スマホでは長いコードを正確に選択しにくい」
「コピーボタンを付けたいけれど、Markdownを全部書き換えたくない」
という問題が出てきます。
ゆまラボでもコード例が増えてきたため、記事内のすべてのコードブロックへ「コピー」ボタンを付けてみました。
ただし、単純にClipboard APIを呼ぶだけではありません。
Clipboard APIが使えない環境へのフォールバック、複数コードブロックへの対応、「コピー済み」の表示、aria-label、ライブリージョン、既存の横スクロールまで実際に確認しています。
この記事では、AstroのMarkdown本文を変更せずにコードブロックへコピーボタンを追加した方法と、実装時に確認したポイントをまとめます。
コードブロックは表示だけで、コピー操作がなかった#
自動デプロイのテスト記事には、FrontmatterやMarkdown本文をまとめた長いコードブロックがある。

既存のコードブロックにはシンタックスハイライトと横スクロールがあり、長い行も表示できる。
ただ、内容をそのまま使う場合はコードをドラッグして選択する必要がある。短いコマンドなら大きな問題にならないが、複数行の設定やMarkdownを丸ごとコピーしたいときは操作が増える。
同じ記事には、短いコードブロックが複数並ぶ箇所もある。

今回の追加では、インラインコードには手を入れず、Markdownから生成された<pre><code>だけを対象にする。
Markdown生成後のpre codeへボタンを追加する#
現在の記事は、render(post)でMarkdownを描画し、ArticleLayout.astroの.prose内へ出力している。
そのため、Markdownの記述方法を変えたり、記事ごとにコピーボタン用のHTMLを書いたりする必要はない。
今回はArticleLayout.astro側で、ページ内に出力された<pre><code>をJavaScriptから取得し、それぞれのコードブロックへボタンを追加する方法にした。
流れは次のようになる。
Markdown
↓
Astroが <pre><code> を生成
↓
ArticleLayoutのスクリプトで対象を取得
↓
各コードブロックへ「コピー」ボタンを追加
記事本文のMarkdownはそのままなので、既存記事を一括修正する作業も発生しない。
Codexで生成HTMLを確認したところ、<pre><code>を含む39記事すべてに拡張用スクリプトが出力されている。確認に使った記事ではコードブロックが5個あり、実行時に5個のボタンが追加された。
Clipboard APIを使い、使えない場合も処理を残す#
コピー処理はnavigator.clipboard.writeText()を優先している。
ブラウザでClipboard APIを利用できる場合は、そのコードブロックの文字列をそのままクリップボードへ渡す。
一方、Clipboard APIを使えない場合にボタンが何もしなくなる構成にはせず、一時的なtextareaを使うフォールバックも追加した。
コピーを押す
↓
navigator.clipboard.writeText()を試す
↓
利用できない場合はtextareaを使う
新しいnpmパッケージは追加していない。Astroの記事レイアウトに小さなクライアントJavaScriptを加えるだけにしている。
JavaScript自体が動かない場合も、コード本文は従来どおりpreとして表示される。コピー操作がなくなるだけで、記事を読めなくなることはない。
コードブロック右上に「コピー」を表示する#
修正後は、コードブロックの右上に「コピー」ボタンが表示される。

既存の横スクロールは残している。
長いコードでは下部のスクロールバーを使って横へ移動でき、コピーボタンはコードブロックの枠内に配置される。コード表示そのものを作り直したわけではない。
ボタンにはaria-labelも設定した。キーボードから操作でき、状態変化を読み上げ側へ伝えるためのライブリージョンも追加している。
コピー後は「コピー済み」へ切り替える#
ボタンを押してコピーに成功すると、表示を「コピー済み」へ変更する。

見た目の変更に合わせて、aria-labelも更新する。
コピー処理には成功時だけでなく失敗時の表示も追加している。今回のローカル確認ではコピーに成功し、実際にコード文字列がクリップボードへ入ることを確認した。
公開後はHTTPS環境でも同じ操作を確認する。Clipboard APIは実際のブラウザ環境にも影響されるため、公開ページでの確認は残しておく。
複数のコードブロックにも個別に付く#
1つの記事に複数のコードブロックがある場合も、それぞれにボタンを追加する。

画像では、
cd C:\000pj\astro\yumaru-site
npm run dev
のブロックと、
http://localhost:4321/blog/cloudflare-test/
のブロックに、それぞれ別の「コピー」が表示されている。
ボタンはpre codeごとに追加される。どのコードをコピーするかを選ぶための別UIは必要ない。
Markdown記事のローカル確認については、自動デプロイのテスト記事にも作業手順を残している。
Astroの記事をGitHub経由でCloudflareへ自動デプロイする方法
TOPの連載表示も既存カードへ合わせた#
同じ作業時に、TOPの連載カードを既存コンポーネントへ合わせた。

記事数や説明は記事データと既存の集計を使い、広い画面では3列、狭い画面では1列になる構成にした。詳細はカテゴリ・連載の説明文を一元管理する方法|一覧をカード表示へ改善に分けている。
ビルドと表示を確認する#
変更後に実行したのは次のコマンド。
npm run build
node scripts/verify.mjs
git diff --check
npm run buildは成功。
当時は158ページを生成し、Pagefindの索引作成とsitemap出力まで完了した。記事数やタグページの構成が変わった現在の件数ではない。
生成HTML、ローカル静的プレビュー、ヘッドレスChromeでも確認している。
コードブロックについては、
pre codeを含む39記事へスクリプトが出力される- 確認記事の5コードブロックに5ボタンが追加される
- コピー後に「コピー済み」へ変わる
- 長いコードでも既存の横スクロールが残る
- ボタンがコードブロックの枠内に収まる
ことを確認した。
TOPでは、
- 3つの連載カードが表示される
- タイトル、説明、記事数、リンクが出る
- 広い画面では3列
- 390px相当では1列
- カードで横スクロールが発生しない
ことを確認した。
今回の変更に起因するビルドエラーは発生していない。既存記事や同時作業中の記事に由来する未登録タグの警告、Pagefindの日本語ステミング通知、Nodeの実験的機能通知はそのまま残している。
コピーボタンと連載カードの変更点#
今回は記事本文とTOPページの2か所を変更した。
- Markdownから生成されたコードブロックへ「コピー」を追加
- コピー成功後は「コピー済み」へ変更
- Clipboard APIが使えない場合のフォールバックを追加
- 1記事内の複数コードブロックへ個別にボタンを追加
- 既存の横スクロール表示を維持
- TOPの「連載から読む」を3列カードへ変更
/series/で使っているカード表示とtaxonomy集計を再利用- 連載の記事数を手入力せず既存の集計から表示
記事本文のMarkdownや既存URL、package.json、Cloudflare Workersの構成には変更を入れていない。


