跳至主要内容

讓 Docusaurus 的 Sidebar 自動計算文章數量

Docusaurus 預設提供了功能非常豐富的 Sidebar,但不合用。

這次讓我們動手改一改。

目標很簡單,原本的 Sidebar 會顯示我們指定的類別,網頁啟動時會去查看每個層級的 _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 的官方文件找找看:


原本預設的 Sidebar,透過 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 = {
// By default, Docusaurus generates a sidebar from the docs folder structure
tutorialSidebar: [{ type: "autogenerated", dirName: "." }],

// But you can create a sidebar manually
/*
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} */
// 這是一個 Docusaurus 側邊欄配置的類型定義,方便 IDE 提供提示
function generateSidebar() {
// 設定基本路徑,這裡指向 'papers' 目錄,可以根據實際情況修改
const baseDir = path.join(__dirname, "papers");

// 讀取 'papers' 目錄下的所有子目錄,過濾掉隱藏的目錄(以 . 開頭的)
const categories = fs.readdirSync(baseDir).filter((item) => {
const itemPath = path.join(baseDir, item);
// 確認是目錄且不是隱藏的
return fs.statSync(itemPath).isDirectory() && !item.startsWith(".");
});

const sidebarItems = []; // 用來存放側邊欄項目的陣列

// 首先加入一個固定的 'intro' 項目
sidebarItems.push("intro");

// 迭代所有類別目錄
categories.forEach((category) => {
const categoryPath = path.join(baseDir, category); // 取得每個類別的完整路徑
const count = countItemsInDirectory(categoryPath); // 計算該目錄中的項目數量

// 嘗試讀取該類別目錄中的 '_category_.json' 檔案,來獲取類別的標籤和連結
const categoryJsonPath = path.join(categoryPath, "_category_.json");
let label = category; // 預設標籤為目錄名稱
let link = undefined; // 預設連結為 undefined
if (fs.existsSync(categoryJsonPath)) {
// 如果 '_category_.json' 存在
const categoryJson = JSON.parse(
fs.readFileSync(categoryJsonPath, "utf8")
); // 讀取並解析 JSON
label = categoryJson.label || category; // 使用 JSON 中的 label,若無則使用目錄名稱
link = categoryJson.link; // 使用 JSON 中的 link
}

// 在標籤後附加項目數量
label = `${label} (${count})`;

// 創建側邊欄項目,type 為 'category',表示這是一個分類
const sidebarItem = {
type: "category",
label: label, // 顯示的標籤
items: [{ type: "autogenerated", dirName: category }], // 自動生成分類下的文件項目
};

if (link) {
// 如果有連結,則添加到該分類中
sidebarItem.link = link;
}

sidebarItems.push(sidebarItem); // 將該分類項目加入側邊欄陣列
});

// 返回一個包含側邊欄配置的物件
return {
papersSidebar: sidebarItems,
};
}

// 計算指定目錄中的有效項目數量(包含子目錄和 Markdown 檔案)
function countItemsInDirectory(dirPath) {
const items = fs.readdirSync(dirPath).filter((item) => {
const itemPath = path.join(dirPath, item);
// 過濾掉 '_category_.json' 檔案和隱藏檔案(以 . 開頭)
if (item === "_category_.json" || item.startsWith(".")) return false;
const stat = fs.statSync(itemPath);
// 只計算目錄和 .md 檔案
return stat.isDirectory() || (stat.isFile() && item.endsWith(".md"));
});
return items.length; // 返回計算的項目數量
}

// 將產生的側邊欄配置匯出,讓 Docusaurus 使用
export default generateSidebar();