コードブロックにコピーボタンを付ける―Clipboard APIとフォールバック対応

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

公開:更新:
コードブロックにコピーボタンを付ける―Clipboard APIとフォールバック対応

技術記事にコードを載せていると、

「このコードをそのまま試したいのに、ドラッグしてコピーするのが面倒」
「スマホでは長いコードを正確に選択しにくい」
「コピーボタンを付けたいけれど、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つの記事に複数のコードブロックがある場合も、それぞれにボタンを追加する。

2つのコードブロックへ個別にコピーボタンを追加した状態

画像では、

cd C:\000pj\astro\yumaru-site
npm run dev

のブロックと、

http://localhost:4321/blog/cloudflare-test/

のブロックに、それぞれ別の「コピー」が表示されている。

ボタンはpre codeごとに追加される。どのコードをコピーするかを選ぶための別UIは必要ない。

Markdown記事のローカル確認については、自動デプロイのテスト記事にも作業手順を残している。

Astroの記事をGitHub経由でCloudflareへ自動デプロイする方法

TOPの連載表示も既存カードへ合わせた#

同じ作業時に、TOPの連載カードを既存コンポーネントへ合わせた。

TOPページの連載から読むを3列カードへ変更した状態

記事数や説明は記事データと既存の集計を使い、広い画面では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の構成には変更を入れていない。