ページ本文へ移動
炭火ブログ

MENU

ねこ ねこ

WordPressでShikiを導入!CDNと遅延読み込みで高速化する実装術

「Prism.js」というシンタックスハイライターを使っていましたが、VS Codeに採用されている「Shiki」を使ってみたいと思いました。このブログはWordPressを利用して作成しているので、プラグインを使えば簡単にできそうですが、プラグインを使わない方法を探していました。

「Prism.js」や「highlight.js」のように分かりやすい方法が見つけられず、何度か挑んだのですが上手く行きませんでした。今回、CDNにアクセスする方法を使い、思ったより短いコードで「Shiki」が使えたので備忘録を残します。

目次
  1. Shikiの導入方法
  2. Shikiを使った各言語の見た目
  3. 最後に

Shikiの導入方法

ページの表示レスポンスを考えてCDNへのアクセスは避けてきたのですが、「Shiki」は手間をかけてサーバーに設置する方法、CDNへアクセスする方法のどちらでも負荷は大きいので、導入の簡単なCDN方式を選びます。また、ファーストビュー領域にコードを書かなければ、シンタックスハイライターはページを読み込むと同時に動作する必要がありません。

加えて、人が読みやすくなるためだけの機能なので、シンタックスハイライターがなくても表示に異常がある訳ではないのでSEOに影響しません。そこで、遅延読込みさせて初期のページ読み込みの負荷を減らします。少しスクロール(50px)したタイミングで「Shiki」を動かします。

言語は「js, css, html, php, json」とそれ以外のために「bash」を使います。Linuxを使っているので、たまに「bash」の記述があるのと、それ以外のコードにも「bash」を指定すると色がつくので、その他の言語用にも使います。テーマの配色は慣れているAntigravityのデフォルト色を探します。

JavaScriptコード

「Prism.js」ではCSSで色を決めていたので、好みに変えられましたが、「Shiki」はJavaScriptでHTMLにスタイル属性を加えて色指定をするので、CSSファイルは不要です。テーマは「slack-dark」が見慣れたテーマでした。「Prism.js」はCSSでは白色が多かったのですが、こちらはすべてに色がついています。

/**
 * シンタックスハイライトライブラリ「Shiki」を遅延読み込みし、
 * ページ内のコードブロックをハイライトするスクリプト。
 */

// 初期化フラグ(2回目以降のスクロール実行を防止するため)
let isHighlighterLoaded = !1;

// スクロールイベントを監視して、必要になったタイミングでShikiを読み込む
window.addEventListener("scroll", async function () {
  // まだ読み込まれておらず、かつスクロール量が50pxを超えた場合に実行
  if (!isHighlighterLoaded && window.scrollY > 50) {
    isHighlighterLoaded = !0; // フラグを立てて重複実行を防止

    try {
      // ShikiライブラリをESモジュールとして動的にインポート
      const shiki = await import("https://esm.sh/shiki");

      // ライブラリのバージョンによって異なる関数名を考慮
      const getHighlighter = shiki.createHighlighter || shiki.getHighlighter;

      // ハイライターを初期化(テーマと使用する言語を指定)
      const highlighter = await getHighlighter({
        themes: ["slack-dark"],
        langs: ["js", "css", "html", "php", "json", "bash"],
      });

      // ページ内のすべての <pre><code> 要素を対象に処理を行う
      document.querySelectorAll("pre code").forEach((codeElement) => {
        // classリストから、サポートしている言語名を探す
        const detectedLang = Array.from(codeElement.classList).find((className) =>
          ["js", "css", "html", "php", "json", "bash"].includes(className),
        );

        // 言語クラスが見つからない場合は処理をスキップ
        if (!detectedLang) return;

        // コードの中身(テキスト)を取得
        const rawCode = codeElement.textContent;

        // 指定した言語とテーマでHTMLを生成
        const highlightedHtml = highlighter.codeToHtml(rawCode, {
          lang: detectedLang,
          theme: "slack-dark",
        });

        // 親要素(通常は <pre>)を取得
        const parentElement = codeElement.parentElement;

        // 一時的なdiv要素を作成して、生成されたHTMLをパースする
        const tempDiv = document.createElement("div");
        tempDiv.innerHTML = highlightedHtml;

        // Shikiが生成した新しい <pre> 要素を取得
        const newPreElement = tempDiv.firstElementChild;

        // 言語名をクラスとして追加(スタイル調整用など)
        newPreElement.classList.add(detectedLang);

        // 元の <pre> 要素を、新しく生成されたハイライト済みの要素に置き換える
        parentElement.replaceWith(newPreElement);
      });
    } catch (error) {
      // 読み込みエラー時の処理(必要に応じてログ出力など)
      console.error("Shiki initialization failed:", error);
    }
  }
});

WordPressでの使い方

WordPressで使うにはコードブロックにコードをペーストし「HTMLとして編集」を選びます。すると以下のようにHTMLが表示されるので

<pre class="wp-block-code"><code>

Codeタグに「class=html」の形で表示言語を指定します。この時にclass="wp-block-code"は「Shiki」が読み込まれると消えるので、残しても消してもどちらでも構いません。言語指定のクラスを加えると「このブロックには、想定されていないか無効なコンテツが含まれています。」と表示されるので右の詳細設定から「HTMLに変換」を選択します。

<pre><code class="html">

Codeタグにクラス名として使用言語を設定しても「Shiki」が読み込まれると消えるので、疑似要素を使い言語名を表示するためにPreタグに言語名をクラス名として付与しています。シンタックスハイライトが有効な状態では以下のようになります。

<pre class="shiki slack-dark html"><code>

Shikiを使った各言語の見た目

JavaScriptとHTML以外の言語の見た目は以下のように表示されます。

CSS

:where(:root[dm] dialog *),
[dn]:where(:root[dm] *) {
    outline: none
}

PHP

// 画像
$imageWidth = $item['images']['primary']['large']['width'];
$imageHeight = $item['images']['primary']['large']['height'];
$imageUrl = $item['images']['primary']['large']['url'];
if ($imageWidth >= $imageHeight) {
    $displayWidthMobile = 250;
    $displayHeightMobile = round(($imageHeight / $imageWidth) * 250);
    $displayWidth = 200;
    $displayHeight = round(($imageHeight / $imageWidth) * 200);
} else {
    $displayHeightMobile = 250;
    $displayWidthMobile = round(($imageWidth / $imageHeight) * 250);
    $displayHeight = 200;
    $displayWidth = round(($imageWidth / $imageHeight) * 200);
}

JSON

{
    "url": "\/water-permeability-of-filter-media\/",
    "page_views": "25"
},
{
    "url": "\/amplifier-to-the-speakers\/",
    "page_views": "23"
},

Bash

sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://us-central1-apt.pkg.dev/doc/repo-signing-key.gpg | \
sudo gpg --dearmor --yes -o /etc/apt/keyrings/antigravity-repo-key.gpg
echo "deb [signed-by=/etc/apt/keyrings/antigravity-repo-key.gpg] https://us-central1-apt.pkg.dev/projects/antigravity-auto-updater-dev/ antigravity-debian main" | \
sudo tee /etc/apt/sources.list.d/antigravity.list > /dev/null

最後に

処理が重いのでコードはファーストビューに表示させないように気をつければ、今までと同じように使えそうです。生成AIが発達しコードを紹介する機会は減ると思いますが、現状では「Shiki」が最も正確できれいなシンタックスハイライターだと思うので使えるようになって良かったです。

最後までお読みいただき、誠にありがとうございます。