LOADING
1587 字
8 分鐘
在側邊欄目錄新增「隨筆」板塊
2026-04-08
統計加載中...

🛠️ Twilight 主題:如何在側邊欄目錄新增「隨筆」板塊 (SOP)

說明:當你在主題中建立了全新的獨立板塊(例如「隨筆」)後,你會發現它預設並不會出現在左側的目錄樹(Directory Widget)中。這是因為目錄組件的資料獲取邏輯是寫死的。

本指南將教你如何修改目錄生成工具,讓「隨筆」以及其底下的所有資料夾,都能完美顯示在側邊欄中。


🎯 核心目標

修改目錄資料生成的工具檔案,手動將我們新建的 notes 集合(Collection)注入到目錄樹中,並確保它支援多層級資料夾嵌套以及正確的絕對路徑跳轉


📂 修改目標檔案

請在你的專案中找到並打開以下檔案: 👉 src/utils/directory.ts


🛠️ 具體修改步驟

1. 引入原生獲取工具

在檔案最頂部的 import 區域,確保你引入了 Astro 原生的 getCollection 方法:

import { getCollection } from "astro:content";

2. 註冊根節點名稱

getDirectoryTree 函數內,找到 rootMap 物件。在這裡為你的新板塊設定一個要在側邊欄顯示的「頂級資料夾名稱」(此處設定為繁體字的「隨筆」):

const rootMap = {
posts: i18n(I18nKey.posts),
// ... 其他原有的分類
timeline: i18n(I18nKey.timeline),
// 👇 新增這一行
notes: "隨筆",
};

3. 抓取資料並注入目錄樹 (支援無限層級)

在處理完 postsfor 迴圈下方,新增一段專門處理 notes 的邏輯。

⚠️ 關鍵邏輯說明:

  • 必須使用 fullSlug 來保留帶有資料夾層級的完整路徑(例如 diary/2026/test),否則點擊會跳轉到 404。
  • 必須將 rootMap.notes 作為陣列的第一項,確保它歸類在「隨筆」這個大資料夾下。
// --- 處理隨筆 (Notes) ---
const allNotes = await getCollection("notes");
for (const note of allNotes) {
if (note.data.draft) continue; // 隱藏草稿
// 1. 分割 ID 獲取資料夾層級
const parts = note.id.split('/');
const fileName = parts.pop()!;
// 2. 獲取不含副檔名的完整相對路徑,確保跳轉正確
const fullSlug = note.id.replace(/\.[^/.]+$/, "");
// 3. 構建目錄樹層級:["隨筆", "子資料夾1", "子資料夾2"...]
const paths = [rootMap.notes, ...parts];
// 4. 將節點加入樹中 (確保 URL 使用絕對路徑 /notes/...)
addNode(
paths,
note.data.title || fileName.replace(/\.[^/.]+$/, ""),
`/notes/${fullSlug}/`
);
}

✅ 完整程式碼參考 (替換即可)

import { getSortedPosts } from "./post";
import { sortedAlbums } from "./albums";
import { sortedMoments } from "./diary";
import { projectsData } from "./projects";
import { skillsData } from "./skills";
import { timelineData } from "./timeline";
import { i18n } from "../i18n/translation";
import I18nKey from "../i18n/i18nKey";
// 引入 Astro 原生获取集合的方法
import { getCollection } from "astro:content";
export interface DirectoryNode {
name: string;
type: 'folder' | 'file';
url?: string;
children?: DirectoryNode[];
}
export async function getDirectoryTree(): Promise<DirectoryNode[]> {
const rootMap = {
posts: i18n(I18nKey.posts),
albums: i18n(I18nKey.albums),
diary: i18n(I18nKey.diary),
projects: i18n(I18nKey.projects),
skills: i18n(I18nKey.skills),
timeline: i18n(I18nKey.timeline),
// 在此处添加“随笔”分类在目录树中的根显示名称
notes: "隨筆",
};
const tree: Record<string, any> = {};
function addNode(paths: string[], name: string, url: string) {
let current = tree;
for (const part of paths) {
if (!current[part]) {
current[part] = { name: part, type: 'folder', children: {} };
}
current = current[part].children;
}
current[name] = { name, type: 'file', url };
}
// --- 1. 处理主博客文章 (Posts) ---
const posts = await getSortedPosts();
for (const post of posts) {
if (post.data.draft) continue;
const parts = post.id.split('/');
const fileName = parts.pop()!;
const paths = [rootMap.posts, ...parts];
addNode(paths, post.data.title || fileName, `/posts/${post.id}/`);
}
// --- 2. 处理随笔 (Notes) - [核心修改:支持子文件夹及正确路径] ---
const allNotes = await getCollection("notes");
for (const note of allNotes) {
if (note.data.draft) continue;
// 分割 ID 获取文件夹层级
const parts = note.id.split('/');
// 弹出文件名
const fileName = parts.pop()!;
// 核心修复:获取不含后缀的完整相对路径 (如 "diary/2026/test")
const fullSlug = note.id.replace(/\.[^/.]+$/, "");
// 构建目录树层级:[“随笔”, "子文件夹1", "子文件夹2"...]
const paths = [rootMap.notes, ...parts];
// 将节点加入树中:
// 参数 1: 文件夹层级
// 参数 2: 显示名称 (优先用标题,没有则用去后缀的文件名)
// 参数 3: 跳转 URL (必须以 /notes/ 开头并包含完整路径)
addNode(
paths,
note.data.title || fileName.replace(/\.[^/.]+$/, ""),
`/notes/${fullSlug}/`
);
}
// --- 3. 处理相册 (Albums) ---
for (const album of sortedAlbums) {
if (!album.visible) continue;
const basePathParts = album.basePath?.split('/') || [];
if (basePathParts[0] === 'content') basePathParts.shift();
if (basePathParts[0] === 'albums') basePathParts[0] = rootMap.albums;
addNode(basePathParts, album.title || album.id, `/albums/${album.id}/`);
}
// --- 4. 处理日记 (Diary) ---
for (const moment of sortedMoments) {
const basePathParts = moment.basePath?.split('/') || [];
if (basePathParts[0] === 'content') basePathParts.shift();
if (basePathParts[0] === 'diary') basePathParts[0] = rootMap.diary;
addNode(basePathParts, moment.title || moment.id, `/diary/`);
}
// --- 5. 处理项目 (Projects) ---
for (const project of projectsData) {
const basePathParts = project.basePath?.split('/') || [];
if (basePathParts[0] === 'content') basePathParts.shift();
if (basePathParts[0] === 'projects') basePathParts[0] = rootMap.projects;
addNode(basePathParts, project.title || project.id, `/projects/`);
}
// --- 6. 处理技能 (Skills) ---
for (const skill of skillsData) {
const basePathParts = skill.basePath?.split('/') || [];
if (basePathParts[0] === 'content') basePathParts.shift();
if (basePathParts[0] === 'skills') basePathParts[0] = rootMap.skills;
addNode(basePathParts, skill.name || skill.id, `/skills/`);
}
// --- 7. 处理时间线 (Timeline) ---
for (const item of timelineData) {
const basePathParts = item.basePath?.split('/') || [];
if (basePathParts[0] === 'content') basePathParts.shift();
if (basePathParts[0] === 'timeline') basePathParts[0] = rootMap.timeline;
addNode(basePathParts, item.title || item.id, `/timeline/`);
}
// 将对象结构的树转换为前端组件需要的数组结构
// function toArray(obj: Record<string, any>): DirectoryNode[] {
// const arr = Object.values(obj).map(node => {
// if (node.type === 'folder') {
// return {
// name: node.name,
// type: 'folder',
// children: toArray(node.children)
// } as DirectoryNode;
// }
// return {
// name: node.name,
// type: 'file',
// url: node.url
// } as DirectoryNode;
// });
// // 排序逻辑:文件夹在前,文件在后,均按名称字母顺序
// arr.sort((a, b) => {
// if (a.type !== b.type) {
// return a.type === 'folder' ? -1 : 1;
// }
// return a.name.localeCompare(b.name);
// });
// return arr;
// }
function toArray(obj: Record<string, any>): DirectoryNode[] {
// 👇 1. 在这里定义你想要的绝对顺序!(可以随意调整位置)
const rootOrder = [
rootMap.posts, // 第 1 位:文章
rootMap.notes, // 第 2 位:随笔
rootMap.diary, // 第 3 位:日记
rootMap.albums, // 相册
rootMap.projects, // 项目
rootMap.skills, // 技能
rootMap.timeline // 时间线
];
const arr = Object.values(obj).map(node => {
if (node.type === 'folder') {
return {
name: node.name,
type: 'folder',
children: toArray(node.children)
} as DirectoryNode;
}
return {
name: node.name,
type: 'file',
url: node.url
} as DirectoryNode;
});
// 👇 2. 核心修改:让排序逻辑优先听从你的安排
arr.sort((a, b) => {
// 规则1:文件夹永远排在文件前面
if (a.type !== b.type) {
return a.type === 'folder' ? -1 : 1;
}
// 检查这两个名字是否在我们的 VIP 顺序表里
const indexA = rootOrder.indexOf(a.name);
const indexB = rootOrder.indexOf(b.name);
// 规则2:如果都在顺序表里,就严格按照表里的排名先后(重点)
if (indexA !== -1 && indexB !== -1) {
return indexA - indexB;
}
// 规则3:如果是其他普通的子文件夹/文件,还是按字母顺序自然排
return a.name.localeCompare(b.name);
});
return arr;
}
return toArray(tree);
}
在側邊欄目錄新增「隨筆」板塊
/posts/在側邊欄目錄新增隨筆板塊/
作者
Ezorin
發布於
2026-04-08
許可協議
CC BY-NC-SA 4.0

部分信息可能已經過時

Profile Image of the Author
Ezorin
Hi

統計加載中...
Announcement
Welcome to my blog!
目錄
文章
隨筆
日記