🦕

Docusaurus 活用のための Tips (2022年2月版)

2022/02/24に公開

静的サイトジェネレータ Docusaurus のマスコットキャラクターである Docusaurus Keytar くん
Docusaurus Keytar くんをすこれ,そして崇めよ

はじめに

Docusaurus,みなさま使いこなされているでしょうか?

まさかもう令和4年にもなってまだ Sphinx を使って奴隷労働に勤しむ人類はいないかと思いますが,各位いかがお過ごしでしょう?

過酷な労働環境でコンピューターに向かいながら働く、俗に「IT土方(デジタル土方)」と呼ばれる男性
現代の「奴隷」に救いはあるのか

約一年前に『ドキュメント作成ツールの決定版!Markdown + React の体験を Docusaurus で』という記事を執筆し,たくさんの方に読んでいただきました.昨年2月時点では α 版でしかなかったこのプロジェクトも,今となっては bata.15 までバージョンアップを重ねており,非常に快適な使い心地が実現されています.私も,執筆前までのチュートリアル体験だけで終えることなく,その後も Docusaurus に定期的に触れ続けてきました.そのおかげで,それなりに知見も貯まりました(とはいえだいぶ Next.js SSG に浮気していたのですが).

https://zenn.dev/ningensei848/articles/docusaurus_intro

本記事では,上記の記事で紹介しきれなかったカスタマイズのアプローチについて,初級・中級・上級といったグレードごとに手法をご紹介します!

バニラ感が否めずイマイチしっくり来ていないビギナー諸兄におかれましては,ぜひ一段階ランクアップのためにご一読いただければと思います.

初級:remark/rehype がクール

Docusaurus は MDX をサポートしているのですが,それは Markdown (.md) も MDX (.mdx) も,両方とも remark + rehype を通じて html+js に書き出しているという技術的な背景[1]があります.

remark - markdown processor powered by plugins

https://github.com/remarkjs/remark

rehype - HTML processor powered by plugins

https://github.com/rehypejs/rehype

これらの「Unified エコシステム」は,好きなようにプラグインを付け足してカスタマイズするのが非常に簡単です.公式でもかなりの数が紹介されています:

例えば remark-gfm は,GitHub で使われている GFM 記法からの変換をサポートする remark プラグインです.本来であれば GitHub 上でしか認識されないはずの Markdown 文字列が,このプラグインを通じて意味のある HTML タグへと変換されています(URL っぽい文字列を検出してアンカーリンクにする,脚注・打ち消し・テーブル・チェックリストをつくるなど).

https://github.com/remarkjs/remark-gfm

あるいは,ただ URL 文字列を見つけてアンカーリンクにするだけでなく,Zenn 記法のリンクカードのように,リッチなコンテンツに見せるアプローチも考えられます.それに近いものを実現するのが @remark-embedder/core@remark-embedder/transformer-oembed です.

https://github.com/remark-embedder/transformer-oembed


これらのプラグインは,docusaurus.config.js 内の各 plugin-content-{blog,docs,pages} 内のオプションから設定することができます(詳細は以下のページからご確認ください):

https://docusaurus.io/docs/markdown-features/plugins

中級:Swizzle は怖くない

シェーカーを振ってカクテルを作っている女性のバーテンダー(マスター・バーメイド) カクテル?なんのこっちゃ……

Docusaurus Themes' components are designed to be replaceable. The replacing is called "swizzle". In Objective C, method swizzling is the process of changing the implementation of an existing selector (method).
In the context of a website, component swizzling means providing an alternative component that takes precedence over the component provided by the theme.

"Swizzle" で検索してなんもわからんとなった Docusaurus ビギナーは数多く居ることと思います.あまり見慣れない・聞き慣れない言葉なのは当然,由来は Objective-C のメソッドだったようです.ズバリ日本語で説明してくれているページがあったので以下に引用します:

Swizzling とは、メソッドの実装を別のものに置き換えることで、メソッドの機能を変更する行為のことで、通常は実行時に行われます。Swizzling を使用したいと思う理由は様々で、イントロスペクション、デフォルトの動作のオーバーライド、あるいは動的なメソッドのロードなどがあります。
By 松川 晋士, New Relic 株式会社 シニアテクニカルサポートエンジニア
cf. https://newrelic.com/jp/blog/best-practices/the-right-way-to-swizzle-in-objective-c


さて,Docusaurus においてはどうなっているのかというと,各 theme で使われているコンポネントを (1) src/theme 以下に書き出す[2] (2) その名前のコンポネントだけは,ライブラリ側でなくローカルに置いてある物を使う ということらしいです.ローカル環境を経由させて少し味付けし,それを元のライブラリ側と「混ぜる」という感覚なのかもしれません(というのが自分の理解だがあっているだろうか).

この機能を活用して,Docusaurus をコンポネント単位でカスタマイズできることがわかります………ましたが,じゃあその 「コンポネント単位」ってのはどうやって探すんだ? というのが次なる障壁となります.たしかに docusaurus swizzle THEME_NAME --list とすればコンポネントの一覧を得ることができますが,それがどこでどのように機能しているのかについてはイマイチわかりません.

となれば,ソースを覗いてみるのがいいでしょう.docusaurus-theme-classic をルートディレクトリとしたときに,src/theme 以下に構成要素コンポネントが並んでいることがわかります.

例えば,「Edit this page ってのが気に入らないから "このページの編集をリクエスト" に変えたい」と思ったとします(この文言を外部から変更するような設定は,今後もわざわざ追加されるようなことはないでしょう).この文言が含まれるコンポネントを探し,Swizzling してローカルにコピーを作った後,文言を上書きした後にビルドし直します:

$ yarn run swizzle @docusaurus/theme-classic EditThisPage --typescript
# => 実行後に src/theme/EditThisPage/index.tsx が出力されていることを確認
src/theme/EditThisPage.tsx
src/theme/EditThisPage.tsx
import React from "react";
import Translate from "@docusaurus/Translate";

import type { Props } from "@theme/EditThisPage";
import IconEdit from "@theme/IconEdit";
import { ThemeClassNames } from "@docusaurus/theme-common";

export default function EditThisPage({ editUrl }: Props): JSX.Element {
  return (
    <a
      href={editUrl}
      target="_blank"
      rel="noreferrer noopener"
      className={ThemeClassNames.common.editThisPage}
    >
      <IconEdit />
      <Translate
        id="theme.common.editThisPage"
        description="The link label to edit the current page"
      >
        Edit this page  {/* <= Change this text */}
      </Translate>
    </a>
  );
}

この EditThisPage コンポネントが src/theme 以下に存在する限り, @docusaurus/theme-classic に含まれる @theme/EditThisPage コンポネントは無視され続けます.そしてもちろん,src/theme/EditThisPage.tsx が削除されれば,@theme/EditThisPage が優先されるようになります(つまり,元の状態に戻ります)

Swizzling コマンドはコンポネントを復元不能な状態に変化させるものではありません(もし不可逆的な変化を引き起こす場合,Swizzle とは命名されないでしょう).「必要なコンポネントをローカルに持ってきてちょこっと味付けしたものと "混ぜる" 」というのは,そういうニュアンスを表現するためのものと言えます.

上級:Plugin はサイコー

いよいよ Docusaurus Plugins を紹介します.こちらのリストにもある通り,公式が提供するプラグインもすでにいくつかあるため,本格的に自作したい場合はまずこちらを参考にすると良いでしょう.

まず手を付けやすいのは,docusaurus.config.js に直接的に処理を書いてしまうことでしょう.設定ファイルは JS として読み込まれるため,比較的小さな処理であればそこに書き込んでしまうのもアリかもしれません.Plugin オブジェクトさえ返してくれれば,どのような関数であってもかまいません.

焦りながら文章を書いている女性会社員
急いでるし,生 JS を直接書き込んじゃえ~

直接的に処理を書く場合
docusaurus.config.js
module.exports = {
  // ...
  plugins: [
    async function myPlugin(context, options) {
      // ...
      return {
        name: 'my-plugin',
        async loadContent() {
          // ...
        },
        async contentLoaded({content, actions}) {
          // ...
        },
        /* other lifecycle API */
      };
    },
  ],
};

流石に設定ファイルに直接書き込んでしまうのは,ファイルが肥大化する原因なのでやめたとします.次に考えられるアプローチは別ファイルとして作ってインポートすることです.仮に src/plugins というフォルダをつくれば,src/plugins/myPlugin.js というファイルに処理を書けばよいでしょう.その後,docusaurus.config.jsplugins 配列の中にプラグインまでの相対パスを置いてください(プロジェクトルート直下に docusaurus.config.js があるなら ./src/plugins/myPlugin).

肝心な中身については,Plugin Method References | Docusaurus を隅から隅まで目を皿のようにして精読するのが一番の近道と思います.とても自由度が高く設定されているため,いくらでも HACK できてしまうでしょう.醍醐味でもある部分だと思うので,ここは一旦個人の裁量に任せます.

例)別ファイルとして作ってインポートする書き方
docusaurus.config.js
module.exports = {
  // ...
  plugins: [
    // without options:
    './myPlugin',
    // or with options:
    ['./myPlugin', options],
  ],
};
src/plugins/myPlugin.js
module.exports = async function myPlugin(context, options) {
  // ...
  return {
    name: 'my-plugin',
    async loadContent() {
      /* ... */
    },
    async contentLoaded({content, actions}) {
      /* ... */
    },
    /* other lifecycle API */
  };
};

「かたぬき」という提灯がかかったお祭りの屋台で一生懸命型ぬきをしている男の子
男の子は型抜きがすき(筆者の私見)

最後に,型が欲しくて仕方がない TypeScript ジャンキーへのハウツーをお伝えします.

現状,プラグインモジュールを TypeScript で書いて直接的に docusaurus.config.js で読み込むことはできません(2022/02/23 現在).コンポネントが TypeScript でかけるならプラグインも †Zero-config† で書けるようになってほしいというのはありがちな願望かと思いますが,ともあれ beta.15 の段階ではまだサポートされていません.

が,そこであきらめるのではなく,だったら自分でトランスパイルすればええやん! というのが開発者のあるべき姿と考えます.

理想的には,docusaurus のビルドが走るたびに,その直前に myPlugin.tsmyPlugin.js へと変換してやればよいでしょう.が,それを tsc にまかせてしまうと,毎回何十秒も待たされる「最悪体験」に直面してしまうかもしれません.

https://swc.rs/

代わりに,swc を採用します.型チェックこそしません[3]が,Rust による速度の暴力で爆速変換を実現してくれます.やったぜ.

swc による設定の詳細
terminal
yarn add --dev @swc/cli @swc/core
package.json
{
  "scripts": {
    "emit": "swc src/plugins -d src --config-file src/plugins/.swcrc",
    "start": "yarn emit && docusaurus start",
    "build": "yarn emit && docusaurus build",
    "swizzle": "docusaurus swizzle",
    "serve": "docusaurus serve"
  }
}
src/plugins/.swcrc
{
    "minify": true,
    "module": {
        "type": "commonjs",
        "strict": true,
        "noInterop": true,
        "ignoreDynamic": true
    },
    "exclude": [
        ".*.js$",
        ".*.map$"
    ],
    "jsc": {
        "externalHelpers": true,
        "parser": {
            "syntax": "typescript",
            "tsx": false,
            "decorators": false,
            "dynamicImport": false
        },
        "target": "es2022",
        "baseUrl": ".",
        "paths": {
            "@site/*": [
                "./*"
            ]
        }
    }
}

終わりに

Docusaurus をカスタマイズしていく方法を三段階に分けてご紹介しました.そうはいってもまだまだ β 版ということもあり,今後はなにかしらの破壊的な変更が加えられるかもしれません.本記事の情報はあくまでも現時点(2022/02/23)での情報であることにご留意ください.

いずれは「ドキュメンテーションジェネレータの比較(英語版)」ってページにも Docusaurus が掲載され,ぶっちぎりの支持を得るような時代が来ることを切に願います.

(本記事の内容も含め Docusaurus について何かわからないことがあれば,お気軽にコメント・ご質問ください.また,もし人事権を持つ方がこの記事とワイのプロフィールを読んで興味持ったら,ぜひともご一報ください……切実に 😢)

卵から生まれたばかりの恐竜の赤ちゃん
ようやくのぼりはじめたばかりだからな このはてしなく遠い 🦖 坂をよ…

脚注
  1. Docusaurus そちのけで「Unified エコシステム」が気になってしまった方は,まずこの記事を読むのがおすすめです ↩︎

  2. いくら swizzle してもローカルにコンポネントを書き出すだけで,実際には本元のコンポネントは失われていない ↩︎

  3. 型検証については,プロジェクトルート直下にある tsconfig.json に対応した ESLint の設定 (.eslintrc.js) を行なうのが良いだろう ↩︎

GitHubで編集を提案

Discussion