fix: remove body field badge, improve sidebar tree styling, use doc titles in homepage tree
CI / typecheck + build (turbo) (push) Canceled after 0s

- STANDARD_KEYS now includes 'body' to prevent it from rendering as a custom field badge
- Sidebar: tree-style with border-l + ml-2 indentation per level, section separators with border-b, better depth cues
- Homepage buildFolderTree: uses doc.title from DB (not path segments), so _index shows 'CTF Writeups' instead of 'Index'
- Sidebar: _index path segments no longer show leading-space titles (slugToTitle filters empty)
This commit is contained in:
asepharyana
2026-08-21 10:39:01 +07:00
parent 251a6e9b4d
commit 989ee9c464
3 changed files with 69 additions and 47 deletions
+2 -1
View File
@@ -325,7 +325,8 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
// Any other key in the document metadata becomes a dynamic badge. // Any other key in the document metadata becomes a dynamic badge.
const STANDARD_KEYS = new Set([ const STANDARD_KEYS = new Set([
"id", "slug", "title", "type", "section", "status", "id", "slug", "title", "type", "section", "status",
"author", "tags", "path", "createdAt", "updatedAt", "extraFields", "author", "tags", "path", "createdAt", "updatedAt",
"extraFields", "body",
]); ]);
// Edit mode: inline form // Edit mode: inline form
+18 -15
View File
@@ -30,7 +30,7 @@ function buildFolderTree(docs: Doc[], section: string): TreeNode[] {
for (const doc of docs.filter((d) => d.section === section)) { for (const doc of docs.filter((d) => d.section === section)) {
const parts = doc.slug.split("/"); const parts = doc.slug.split("/");
// parts[0] should be the section // parts[0] is the section; skip it
let current = root; let current = root;
for (let i = 1; i < parts.length; i++) { for (let i = 1; i < parts.length; i++) {
@@ -56,7 +56,7 @@ function buildFolderTree(docs: Doc[], section: string): TreeNode[] {
} else if (!node.title) { } else if (!node.title) {
node.title = part node.title = part
.split(/[-_]/) .split(/[-_]/)
.map((w) => w.charAt(0).toUpperCase() + w.slice(1)) .map((w) => (w ? w.charAt(0).toUpperCase() + w.slice(1) : ""))
.join(" "); .join(" ");
} }
current = node.children; current = node.children;
@@ -91,6 +91,7 @@ function buildFolderTree(docs: Doc[], section: string): TreeNode[] {
/** /**
* Recursively render the folder tree with proper depth styling. * Recursively render the folder tree with proper depth styling.
* Each level gets a 20px left margin + subtle border for visual hierarchy.
*/ */
function renderTreeNode( function renderTreeNode(
node: TreeNode, node: TreeNode,
@@ -100,24 +101,24 @@ function renderTreeNode(
const isFolder = node.children.length > 0; const isFolder = node.children.length > 0;
const isActive = pathname === `/${node.slug}`; const isActive = pathname === `/${node.slug}`;
const hasActiveChild = pathname.startsWith(`/${node.slug}/`); const hasActiveChild = pathname.startsWith(`/${node.slug}/`);
const indent = depth * 16; // 16px per level
return ( return (
<li key={node.slug}> <li key={node.slug} className={depth > 0 ? "ml-5 border-l border-[#1f2022] pl-2" : ""}>
<Link <Link
href={`/${node.slug}`} href={`/${node.slug}`}
className={`flex items-center gap-1.5 text-xs py-1 px-2 rounded transition-all ${ className={`flex items-center justify-between gap-1.5 text-xs py-1 px-2 rounded transition-all ${
isActive isActive
? "text-[#7170ff] bg-[#191a1b]" ? "text-[#7170ff] bg-[#191a1b]"
: hasActiveChild : hasActiveChild
? "text-[#d0d6e0]" ? "text-[#d0d6e0]"
: "text-[#8a8f98] hover:text-[#d0d6e0] hover:bg-[#191a1b]" : "text-[#8a8f98] hover:text-[#d0d6e0] hover:bg-[#191a1b]"
}`} }`}
style={{ marginLeft: `${indent}px` }}
title={node.title} title={node.title}
> >
<span>{isFolder ? "📁" : "📄"}</span> <span className="flex items-center gap-1.5 truncate">
<span className="truncate">{node.title || node.name}</span> <span>{isFolder ? "📁" : "📄"}</span>
{node.title || node.name}
</span>
{isFolder && node.docCount > 0 && ( {isFolder && node.docCount > 0 && (
<span className="ml-auto text-[#62666d] bg-[#191a1b] px-1 py-0 rounded"> <span className="ml-auto text-[#62666d] bg-[#191a1b] px-1 py-0 rounded">
{node.docCount} {node.docCount}
@@ -125,7 +126,7 @@ function renderTreeNode(
)} )}
</Link> </Link>
{node.children.length > 0 && ( {node.children.length > 0 && (
<ul className="mt-0.5"> <ul className="mt-0.5 space-y-0.5">
{node.children.map((child) => renderTreeNode(child, depth + 1, pathname))} {node.children.map((child) => renderTreeNode(child, depth + 1, pathname))}
</ul> </ul>
)} )}
@@ -156,7 +157,7 @@ export default function Sidebar() {
{docs.length} documents {docs.length} documents
</span> </span>
</div> </div>
<ul className="space-y-2 px-3 text-sm"> <ul className="space-y-3 px-3 text-sm">
{SECTIONS.map(({ id, label, icon }) => { {SECTIONS.map(({ id, label, icon }) => {
const sectionDocs = docs.filter((d) => d.section === id); const sectionDocs = docs.filter((d) => d.section === id);
if (sectionDocs.length === 0) return null; if (sectionDocs.length === 0) return null;
@@ -165,16 +166,18 @@ export default function Sidebar() {
const isActive = pathname === `/${id}` || pathname.startsWith(`/${id}/`); const isActive = pathname === `/${id}` || pathname.startsWith(`/${id}/`);
return ( return (
<li key={id}> <li key={id} className="border-b border-[#141516] pb-2 last:border-0">
<div <div
className={`flex items-center gap-1.5 mb-1 mt-4 first:mt-0 ${ className={`flex items-center gap-1.5 mb-1.5 ${
isActive ? "text-[#7170ff]" : "text-[#62666d]" isActive ? "text-[#7170ff]" : "text-[#62666d]"
}`} }`}
> >
<span>{icon}</span> <span className="text-lg">{icon}</span>
<span className="text-xs font-medium">{label}</span> <span className="font-medium text-xs uppercase tracking-wider">
{label}
</span>
</div> </div>
<ul className="space-y-0.5 pl-0"> <ul className="space-y-0.5">
{tree.map((node) => renderTreeNode(node, 0, pathname))} {tree.map((node) => renderTreeNode(node, 0, pathname))}
</ul> </ul>
</li> </li>
+49 -31
View File
@@ -5,8 +5,8 @@ import { SECTIONS } from "@mcpedia/config/sections";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
/** /**
* Build a hierarchical folder tree from a flat list of document paths. * Build a hierarchical folder tree from document list.
* Each tree node is either a folder (has children) or a leaf doc. * Each node is either a folder (has children) or a leaf doc.
*/ */
interface TreeNode { interface TreeNode {
name: string; name: string;
@@ -16,16 +16,25 @@ interface TreeNode {
isLeaf: boolean; isLeaf: boolean;
} }
function buildFolderTree(paths: string[], section: string): TreeNode[] { // Helper: turn a slug segment into a display title
const tree: TreeNode[] = []; function slugToTitle(segment: string): string {
const base = `${section}/`; return segment
.split(/[-_]/)
.map((w) => (w ? w.charAt(0).toUpperCase() + w.slice(1) : ""))
.join(" ");
}
const addPath = (cleanPath: string) => { function buildFolderTree(docs: { slug: string; title: string }[], section: string): TreeNode[] {
const parts = cleanPath.split("/"); const tree: TreeNode[] = [];
const treeIndex = new Map<string, TreeNode>();
for (const doc of docs) {
const parts = doc.slug.split("/");
// parts[0] should be the section
let current = tree; let current = tree;
let currentPath = section; let currentPath = section;
for (let i = 0; i < parts.length; i++) { for (let i = 1; i < parts.length; i++) {
const part = parts[i]; const part = parts[i];
const isLeaf = i === parts.length - 1; const isLeaf = i === parts.length - 1;
currentPath = `${currentPath}/${part}`; currentPath = `${currentPath}/${part}`;
@@ -35,36 +44,43 @@ function buildFolderTree(paths: string[], section: string): TreeNode[] {
node = { node = {
name: part, name: part,
slug: currentPath, slug: currentPath,
title: "", title: isLeaf ? doc.title : slugToTitle(part),
children: [], children: [],
isLeaf: false, isLeaf: false,
}; };
current.push(node); current.push(node);
treeIndex.set(currentPath, node);
} }
if (isLeaf) { if (isLeaf) {
node.isLeaf = true; node.isLeaf = true;
node.title = part node.title = doc.title;
.split(/[-_]/)
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(" ");
} else if (!node.title) { } else if (!node.title) {
node.title = part node.title = slugToTitle(part);
.split(/[-_]/)
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(" ");
} }
current = node.children; current = node.children;
} }
};
for (const docPath of paths) {
const rel = docPath.startsWith(base) ? docPath.slice(base.length) : docPath;
const cleanPath = rel.replace(/\.md$/, "");
addPath(cleanPath);
} }
return tree; // Sort: folders first, then docs, alphabetically
function sortNodes(nodes: TreeNode[]): TreeNode[] {
return nodes.sort((a, b) => {
const aFolder = a.children.length > 0 ? 0 : 1;
const bFolder = b.children.length > 0 ? 0 : 1;
if (aFolder !== bFolder) return aFolder - bFolder;
return a.title.localeCompare(b.title);
});
}
function sortAll(nodes: TreeNode[]): TreeNode[] {
const sorted = sortNodes(nodes);
for (const node of sorted) {
if (node.children.length > 0) sortAll(node.children);
}
return sorted;
}
return sortAll(tree);
} }
function renderTree(nodes: TreeNode[], pathname: string) { function renderTree(nodes: TreeNode[], pathname: string) {
@@ -82,10 +98,10 @@ function renderTree(nodes: TreeNode[], pathname: string) {
title={node.title} title={node.title}
> >
<span>{node.isLeaf && node.children.length === 0 ? "📄" : "📁"}</span> <span>{node.isLeaf && node.children.length === 0 ? "📄" : "📁"}</span>
{node.title || node.name} <span className="truncate">{node.title || node.name}</span>
</Link> </Link>
{node.children.length > 0 && ( {node.children.length > 0 && (
<div className="ml-3 mt-0.5"> <div className="ml-5 border-l border-[#1f2022] pl-2 mt-0.5">
{renderTree(node.children, pathname)} {renderTree(node.children, pathname)}
</div> </div>
)} )}
@@ -97,12 +113,15 @@ function renderTree(nodes: TreeNode[], pathname: string) {
interface SectionTreeProps { interface SectionTreeProps {
section: string; section: string;
docPaths: string[]; docs: { slug: string; title: string }[];
pathname: string; pathname: string;
} }
function SectionTree({ section, docPaths, pathname }: SectionTreeProps) { function SectionTree({ section, docs, pathname }: SectionTreeProps) {
const tree = buildFolderTree(docPaths.filter((p) => p.startsWith(`${section}/`)), section); const tree = buildFolderTree(
docs.filter((d) => d.slug.startsWith(`${section}/`)),
section,
);
const sectionInfo = SECTIONS.find((s) => s.id === section); const sectionInfo = SECTIONS.find((s) => s.id === section);
if (!sectionInfo) return null; if (!sectionInfo) return null;
@@ -123,7 +142,6 @@ function SectionTree({ section, docPaths, pathname }: SectionTreeProps) {
export default async function HomePage() { export default async function HomePage() {
const all = await listDocuments(); const all = await listDocuments();
const docPaths = all.map((d) => d.path);
const recent = [...all] const recent = [...all]
.sort( .sort(
@@ -194,7 +212,7 @@ export default async function HomePage() {
{/* Inline folder tree */} {/* Inline folder tree */}
<SectionTree <SectionTree
section={s.id} section={s.id}
docPaths={docPaths} docs={all.map((d) => ({ slug: d.slug, title: d.title }))}
pathname="/" pathname="/"
/> />
</div> </div>