Back to posts

Markdown記事のコードブロック運用メモ(Astro+Shiki前提)

技術記事では、コードブロックは挿絵ではなく本文そのものです。このサイトの記事は Markdown で書いていて、シンタックスハイライトは Astro 標準の Shiki に任せています。サイト側のスタイルの組み立ては Astro+Tailwind CSS 4の組み立て方 の構成ですが、この記事ではコードブロック側の運用に絞ります。設定を大きく作り込む前に、「書き手が守るルール」と「標準のままではできないこと」を分けておくと運用がぶれません。先に前提を表にしておきます。

やりたいことAstro+Shiki 標準備考
言語を指定して色付け○デフォルトテーマは github-dark
ライト/ダークの2テーマ出力○shikiConfig.themes + CSS 変数で切替
行ハイライト・差分の色付け△@shikijs/transformers の追加が必要。色付け用の CSS は自前
ファイル名(タイトル)の表示×フェンスの標準機能には無い
コピーボタン×小さなスクリプト自作か、Expressive Code 等の統合が別途必要
行番号×transformer で出す方法はあるが、ブログでは基本使わない

前提: Shiki はビルド時にハイライト済みの HTML を出すので、読み手側にハイライト用の JavaScript は要りません。凝ったことをしなければ、記事側の書き方ルールだけで大抵は足ります。

言語指定は必ず付ける

フェンスを開いたら、言語名を必ず書きます。付けないと色付けが効かないだけでなく、読み手が「これは設定ファイルなのか、実行するコードなのか」を毎回推測することになります。

```ts
const posts = await getCollection('blog');
```

運用上の細かい決め事はこのあたりです。

  • 実行コマンドは sh、その出力は text と分ける(次節で詳しく)
  • 設定ファイルは中身に合う言語名にする(json、yaml、toml を使い分け)
  • 差分を見せたいだけのときは diff 言語に逃げず、通常の言語名+行ハイライトで書く

diff は赤と緑しか出ないので、構文の色付けが消えてかえって読みにくくなります。差分の表現方法は後述します。

長いコードは「抜粋+全文の置き場」をセットにする

記事に貼るコードは、説明に必要な範囲だけに切り取ります。判断の目安は単純で、そのブロックを読み手がスクロールし終わる前に、文章側の説明が何を言いたかったか忘れてしまう長さなら長い、というものです。

  • 抜粋するときは、省略した箇所に // ... のような印を必ず入れる。黙って削ると「この変数はどこから来たのか」が追えなくなる
  • 全文が必要なコードは、リポジトリへのリンクか別ページに置き、記事内では要点の抜粋に留める
  • 設定ファイルは全文が短いことが多いので、丸ごと貼ってよい。ただし秘密情報の値はダミーに置き換える

注意: 抜粋したコードは、そのままコピーしても動かないことがあります。本文側で「抜粋であること」と「全文の置き場」を明記しておくのが、読み手への最低限の案内です。

ファイル名は標準では出ないので、書き方で補う

フェンスにファイル名を書いても、Shiki 標準の出力にはタイトル欄は付きません。ここを割り切って、書き方で補っています。

一番手軽なのは、コードの1行目にパスをコメントで入れる方法です。

// src/content.config.ts
export const collections = { blog };

長い記事でファイルが何度も切り替わる場合は、フェンスの直前の文章で「次は astro.config.mjs の変更です」と明示します。どちらも地味ですが、表示の仕組みに手を入れずに済みます。

どうしてもエディタ風のタイトル欄が欲しい場合は、Expressive Code のような統合を入れる選択肢があります。Astro 公式ドキュメントでも、テキストのマーキングや注釈を増やしたい場合の拡張として名前が挙がっているものです。ただし統合を増やすと、記法や CSS の管理対象も増えます。「ファイル名表示のためだけ」に導入するのは割に合わない、というのが今の判断です。

行ハイライトと差分は transformers で足す

「この2行が今回の変更点です」を伝える行ハイライトは、標準の Shiki だけでは使えません。@shikijs/transformers を入れて、markdown.shikiConfig に登録します。

// astro.config.mjs
import { defineConfig } from 'astro/config';
import {
  transformerMetaHighlight,
  transformerNotationDiff,
} from '@shikijs/transformers';

export default defineConfig({
  markdown: {
    shikiConfig: {
      transformers: [transformerMetaHighlight(), transformerNotationDiff()],
    },
  },
});

登録後は、フェンスの言語名の後ろにメタ文字列で対象行を指定できます。

```ts {2}
const title = post.data.title;
const date = post.data.date;
```

差分は、行末コメントの記法で書けます(コメント自体は表示から消えます)。

console.log('hewwo'); // [!code --]
console.log('hello'); // [!code ++]

注意: transformers は対象の行や単語にクラス(highlighted、diff など)を付けるだけで、見た目の色は付きません。背景色などの CSS は自分で書く必要があります。クラス名ベースなので、サイト側の CSS とセットで初めて機能する、と理解しておくのが安全です。

コピーボタンは「入れるなら小さく」が原則

コピーボタンも標準にはありません。読み手の手間を考えると便利なのですが、判断は次のように分けています。

観点判断
短いコマンド(1〜2行)ボタンがなくても困らない。付けない
長めの設定ファイル・スクリプト付いていると助かる。入れるならこの用途に絞る
実装方法pre 要素にボタンを足す小さなスクリプトで十分。外部の仕組みを持ち込まない

入れる場合の最小形はこの程度です。Shiki の出力は pre.astro-code になるので、そこにボタンを差し込みます。

document.querySelectorAll('pre.astro-code').forEach((pre) => {
  const button = document.createElement('button');
  button.textContent = 'コピー';
  button.addEventListener('click', () => {
    navigator.clipboard.writeText(pre.querySelector('code')?.textContent ?? '');
  });
  pre.append(button);
});

もう1つ大事なのは、コピーされる中身の側のルールです。コマンドの先頭に $ のようなプロンプト記号を付けると、そのままコピーされて実行の邪魔になります。このサイトではコマンドにプロンプト記号を付けない、と決めています。ボタン以前に、コピーに耐える書き方をしておく方が効きます。

コマンドと実行結果はブロックを分ける

ターミナルの話を書くときは、入力と出力を混ぜません。

npm run build
[build] 42 page(s) built in 1.23s

混ぜると、読み手が「どこまでが打つものか」を目で仕分けることになりますし、コピーのときにも混入します。セッション全体を見せたい例外的な場合だけ、1ブロックにまとめて、その旨を文章で断ります。

ダークモードは2テーマを CSS 変数で切り替える

テーマを1つに固定すると、サイトの表示モードとコードの色がちぐはぐになります。shikiConfig でライト/ダークの2テーマを指定すると、Shiki が両方の色を CSS 変数として出力するので、あとはサイト側のモードに合わせて変数を適用するだけです。

export default defineConfig({
  markdown: {
    shikiConfig: {
      themes: {
        light: 'github-light',
        dark: 'github-dark',
      },
      defaultColor: false,
    },
  },
});

Astro ではコードブロックのクラスが公式ドキュメントの例の .shiki ではなく .astro-code になる点だけ、写経するときに注意が必要です。なお、ここで触れているのはコードブロック側の色切り替えだけで、サイト全体のダークモードの設計は Astroサイトのダークモード実装メモ に分けています。

モバイルでは「横スクロール」を許す

長い行がスマートフォンの幅を超えたときの選択肢は2つあります。

方法設定向いているケース
横スクロールpre に overflow-x: autoコード全般。インデント構造が崩れない
折り返しshikiConfig.wrap: trueURL や1行の長い文字列が中心のブロック

コードは折り返すとインデントの対応関係が読み取りにくくなるので、基本は横スクロールに寄せています。折り返しは例外扱いです。全体の設定はこうなります。

.astro-code {
  overflow-x: auto;
  padding: 1rem;
}

投稿前のチェックリスト

最後に、記事を公開する前にコードブロックだけ見直す項目を置いておきます。

  • すべてのフェンスに言語名が付いているか
  • コマンドと実行結果が別ブロックになっているか
  • 抜粋したコードに省略の印と、全文の置き場の案内があるか
  • ファイル名が、コメントか前後の文章で分かるようになっているか
  • ハイライトした行が、本文の説明と一致しているか
  • スマートフォン幅で、意図しないはみ出しがないか

コードブロックの読みやすさは、凝った装飾よりも、この程度の地味なルールの積み重ねで決まる、というのが今のところの結論です。

参考