title

QwikCityでMicroCMSから受け取った記事をシンタックスハイライトする

2023/12/22

FrontendQwik

QwikCityは設定いらずでMDXを全部いい感じに出力してくれますが、ヘッドレスCMS等から受け取った記事本体をPrismでシンタックスハイライトさせるパターンを試してみました。


Prism

Prismはhighlight.jsやshikiに並ぶシンタックスハイライトライブラリです。比較的軽量なところやいくつかのテーマがあるのが特徴で、QwikCityでもMDX利用時のシンタックスハイライターとして使用されています。

Github - PrismJS/Prism

cheerio

cheerioはいわゆるHTML/XMLパーサーで、パース後はjQuery風のセレクタでDOMを操作出来ます。今回のケースだと、ヘッドレスCMSのAPIから受け取ったHTML形式の文字列をパースするのに使用します。

Github - cheeriojs/cheerio

使い方

といっても単純にCheerioとPrismを普通に使用するだけです。今回はsrc/libs/utils.tsというファイルを作成し、そこに処理を書き出してみました。

import * as cheerio from "cheerio";
import Prism from "prismjs";
import "prismjs/components/prism-jsx";
import "prismjs/components/prism-markup";
import "prismjs/components/prism-tsx";
import "prismjs/components/prism-typescript";

export const syntaxHighlight = (richText: string): string => {
  const $ = cheerio.load(richText);
  const highlight = (text: string, language?: string) => {
    // eslint-disable-next-line
    if (!language || !Prism.languages[language]) {
      return Prism.highlight(text, Prism.languages.plain, "plain");
    }
    return Prism.highlight(text, Prism.languages[language], language);
  };

  $("pre code").each((_, elm) => {
    const classAttr = $(elm).attr("class");
    const language = classAttr ? classAttr.replace(/^language-/, "") : "plain";
    const res = highlight($(elm).text(), language);
    $(elm).html(res);
  });

  return $.html();
};

注意が必要な点として、デフォルトで対応している言語(JavaScript等)以外は個別でimportをする必要があり、加えて一部の言語は依存関係のある言語が存在し、それを一緒に読み込む必要があります。

上記の例だと、tsxはjsxに依存していて、jsxはmarkupに依存しているので全てimportしないといけません。

記事取得で使用する

ハイライトするメソッドが作成できたら、残りは記事取得時に記事部分を先程のメソッドに渡すだけです。

以下はMicroCMSのクライアントから取得した記事のコンテンツ部をそのまま渡しているものです。

export const useArticle = routeLoader$(async ({ env, params, status }) => {
  if (!params.articleId) {
    throw new Error("articleId is required");
  }
  const client = getClient(env);
  try {
    const article = await client.getListDetail<Blog>({
      endpoint: CMS_ENDPOINTS.Blog,
      contentId: params.articleId,
    });
    article.content = await syntaxHighlight(article.content);
    return article;
  } catch {
    status(404);
  }
});

おわりに

実はshikiを試して記事にしようとしたのですが、SSR等にはあまり向いていないみたいでうまくCloudflarePagesにデプロイまで持っていけませんでした。

色々と試すことが出来たのは面白かったですし、Prismでも十分だと思うのでしばらくこのままで行ってみようと思います。