メインコンテンツまでスキップ

Docusaurusのサイドバーに記事数を自動計算させる

· 約5分
Zephyr
Engineer

Docusaurus はデフォルトで非常に豊富な機能を持つサイドバーを提供していますが、正直に言えば、少し物足りません。

そこで、今回は手を加えて改良してみます。

目標はシンプルです。既存のサイドバーでは、指定したカテゴリが表示され、起動時に各階層の_category_.jsonを確認します。このファイルの内容は以下のようになります:

{
"label": "Classic CNNs",
"link": {
"type": "generated-index"
}
}

その結果、以下のように表示されます:

  • Classic CNN
  • ...(他のカテゴリ)

ここで、各フォルダの下にある記事数を計算し、ウェブページ上に表示したいと思います。たとえば、以下のように:

  • Classic CNNs (8)
  • ...(他のカテゴリ)

では、_category_.jsonファイル内に数を直接追加すれば良いのでしょうか?

{
"label": "Classic CNNs (8)",
"link": {
"type": "generated-index"
}
}

そして、記事を更新するたびに手動で数を更新する?

そんなことはできません!私たちは恥ずかしいコードを書くわけにはいきません。

参考資料

この問題を解決するため、いつものように Docusaurus の公式ドキュメントを参照します:


デフォルトのサイドバーは、autogeneratedを使用して自動生成されています:

/**
* Creating a sidebar enables you to:
- create an ordered group of docs
- render a sidebar for each doc of that group
- provide next/previous navigation

The sidebars can be generated from the filesystem, or explicitly defined here.

Create as many sidebars as you want.
*/

// @ts-check

/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
const sidebars = {
// デフォルトでは、Docusaurusはdocsフォルダ構造からサイドバーを生成
tutorialSidebar: [{ type: "autogenerated", dirName: "." }],

// 手動でサイドバーを作成する場合
/*
tutorialSidebar: [
'intro',
'hello',
{
type: 'category',
label: 'Tutorial',
items: ['tutorial-basics/create-a-document'],
},
],
*/
};

export default sidebars;

一通り見ましたが、現時点で使えるものはなさそうです。では、自分で書きましょう。

実装

コード内にコメントを含めて書いていますので、必要な場合は以下の箇所を変更してください:

  1. 8 行目: const baseDir = path.join(__dirname, "papers");

    • papersはフォルダ名を指します。正しいフォルダパスに変更してください。

  1. 20 行目: sidebarItems.push("intro");

    • introはトップページの名前です。トップページがない場合は、この行を削除してください。

  1. 72 行目: return stat.isDirectory() || (stat.isFile() && item.endsWith(".md"));

    • .mdは記事のファイル形式を指します。必要に応じて変更してください。

以下に実装コードを示します。結果は直接ウェブページで確認してください:Papers

/sidebars.js
const fs = require("fs"); // Node.jsのファイルシステムモジュールを使用
const path = require("path"); // Node.jsのパスモジュールを使用

/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
function generateSidebar() {
const baseDir = path.join(__dirname, "papers");

const categories = fs.readdirSync(baseDir).filter((item) => {
const itemPath = path.join(baseDir, item);
return fs.statSync(itemPath).isDirectory() && !item.startsWith(".");
});

const sidebarItems = [];
sidebarItems.push("intro");

categories.forEach((category) => {
const categoryPath = path.join(baseDir, category);
const count = countItemsInDirectory(categoryPath);

const categoryJsonPath = path.join(categoryPath, "_category_.json");
let label = category;
let link = undefined;

if (fs.existsSync(categoryJsonPath)) {
const categoryJson = JSON.parse(
fs.readFileSync(categoryJsonPath, "utf8")
);
label = categoryJson.label || category;
link = categoryJson.link;
}

label = `${label} (${count})`;

const sidebarItem = {
type: "category",
label: label,
items: [{ type: "autogenerated", dirName: category }],
};

if (link) {
sidebarItem.link = link;
}

sidebarItems.push(sidebarItem);
});

return {
papersSidebar: sidebarItems,
};
}

function countItemsInDirectory(dirPath) {
const items = fs.readdirSync(dirPath).filter((item) => {
const itemPath = path.join(dirPath, item);
if (item === "_category_.json" || item.startsWith(".")) return false;
const stat = fs.statSync(itemPath);
return stat.isDirectory() || (stat.isFile() && item.endsWith(".md"));
});
return items.length;
}

export default generateSidebar();