1587 字
8 分鐘
在側邊欄目錄新增「隨筆」板塊
🛠️ 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. 抓取資料並注入目錄樹 (支援無限層級)
在處理完 posts 的 for 迴圈下方,新增一段專門處理 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);}部分信息可能已經過時