From 989ee9c4646d6efddadfc97657a2ad722589fe71 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Fri, 21 Aug 2026 10:39:01 +0700 Subject: [PATCH] fix: remove body field badge, improve sidebar tree styling, use doc titles in homepage tree - 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) --- apps/web/app/[section]/[...slug]/page.tsx | 3 +- apps/web/app/components/Sidebar.tsx | 33 +++++----- apps/web/app/page.tsx | 80 ++++++++++++++--------- 3 files changed, 69 insertions(+), 47 deletions(-) diff --git a/apps/web/app/[section]/[...slug]/page.tsx b/apps/web/app/[section]/[...slug]/page.tsx index b3df3fa..e365199 100644 --- a/apps/web/app/[section]/[...slug]/page.tsx +++ b/apps/web/app/[section]/[...slug]/page.tsx @@ -325,7 +325,8 @@ export default async function DocPage({ params, searchParams }: DocPageProps) { // Any other key in the document metadata becomes a dynamic badge. const STANDARD_KEYS = new Set([ "id", "slug", "title", "type", "section", "status", - "author", "tags", "path", "createdAt", "updatedAt", "extraFields", + "author", "tags", "path", "createdAt", "updatedAt", + "extraFields", "body", ]); // Edit mode: inline form diff --git a/apps/web/app/components/Sidebar.tsx b/apps/web/app/components/Sidebar.tsx index cc77421..196cdcc 100644 --- a/apps/web/app/components/Sidebar.tsx +++ b/apps/web/app/components/Sidebar.tsx @@ -30,7 +30,7 @@ function buildFolderTree(docs: Doc[], section: string): TreeNode[] { for (const doc of docs.filter((d) => d.section === section)) { const parts = doc.slug.split("/"); - // parts[0] should be the section + // parts[0] is the section; skip it let current = root; for (let i = 1; i < parts.length; i++) { @@ -56,7 +56,7 @@ function buildFolderTree(docs: Doc[], section: string): TreeNode[] { } else if (!node.title) { node.title = part .split(/[-_]/) - .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .map((w) => (w ? w.charAt(0).toUpperCase() + w.slice(1) : "")) .join(" "); } current = node.children; @@ -91,6 +91,7 @@ function buildFolderTree(docs: Doc[], section: string): TreeNode[] { /** * Recursively render the folder tree with proper depth styling. + * Each level gets a 20px left margin + subtle border for visual hierarchy. */ function renderTreeNode( node: TreeNode, @@ -100,24 +101,24 @@ function renderTreeNode( const isFolder = node.children.length > 0; const isActive = pathname === `/${node.slug}`; const hasActiveChild = pathname.startsWith(`/${node.slug}/`); - const indent = depth * 16; // 16px per level return ( -
  • +
  • 0 ? "ml-5 border-l border-[#1f2022] pl-2" : ""}> - {isFolder ? "📁" : "📄"} - {node.title || node.name} + + {isFolder ? "📁" : "📄"} + {node.title || node.name} + {isFolder && node.docCount > 0 && ( {node.docCount} @@ -125,7 +126,7 @@ function renderTreeNode( )} {node.children.length > 0 && ( -
      +
        {node.children.map((child) => renderTreeNode(child, depth + 1, pathname))}
      )} @@ -156,7 +157,7 @@ export default function Sidebar() { {docs.length} documents -
        +
          {SECTIONS.map(({ id, label, icon }) => { const sectionDocs = docs.filter((d) => d.section === id); if (sectionDocs.length === 0) return null; @@ -165,16 +166,18 @@ export default function Sidebar() { const isActive = pathname === `/${id}` || pathname.startsWith(`/${id}/`); return ( -
        • +
        • - {icon} - {label} + {icon} + + {label} +
          -
            +
              {tree.map((node) => renderTreeNode(node, 0, pathname))}
            diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index a830bc7..12869e7 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -5,8 +5,8 @@ import { SECTIONS } from "@mcpedia/config/sections"; export const dynamic = "force-dynamic"; /** - * Build a hierarchical folder tree from a flat list of document paths. - * Each tree node is either a folder (has children) or a leaf doc. + * Build a hierarchical folder tree from document list. + * Each node is either a folder (has children) or a leaf doc. */ interface TreeNode { name: string; @@ -16,16 +16,25 @@ interface TreeNode { isLeaf: boolean; } -function buildFolderTree(paths: string[], section: string): TreeNode[] { - const tree: TreeNode[] = []; - const base = `${section}/`; +// Helper: turn a slug segment into a display title +function slugToTitle(segment: string): string { + return segment + .split(/[-_]/) + .map((w) => (w ? w.charAt(0).toUpperCase() + w.slice(1) : "")) + .join(" "); +} - const addPath = (cleanPath: string) => { - const parts = cleanPath.split("/"); +function buildFolderTree(docs: { slug: string; title: string }[], section: string): TreeNode[] { + const tree: TreeNode[] = []; + const treeIndex = new Map(); + + for (const doc of docs) { + const parts = doc.slug.split("/"); + // parts[0] should be the section let current = tree; let currentPath = section; - for (let i = 0; i < parts.length; i++) { + for (let i = 1; i < parts.length; i++) { const part = parts[i]; const isLeaf = i === parts.length - 1; currentPath = `${currentPath}/${part}`; @@ -35,36 +44,43 @@ function buildFolderTree(paths: string[], section: string): TreeNode[] { node = { name: part, slug: currentPath, - title: "", + title: isLeaf ? doc.title : slugToTitle(part), children: [], isLeaf: false, }; current.push(node); + treeIndex.set(currentPath, node); } if (isLeaf) { node.isLeaf = true; - node.title = part - .split(/[-_]/) - .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) - .join(" "); + node.title = doc.title; } else if (!node.title) { - node.title = part - .split(/[-_]/) - .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) - .join(" "); + node.title = slugToTitle(part); } 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) { @@ -82,10 +98,10 @@ function renderTree(nodes: TreeNode[], pathname: string) { title={node.title} > {node.isLeaf && node.children.length === 0 ? "📄" : "📁"} - {node.title || node.name} + {node.title || node.name} {node.children.length > 0 && ( -
            +
            {renderTree(node.children, pathname)}
            )} @@ -97,12 +113,15 @@ function renderTree(nodes: TreeNode[], pathname: string) { interface SectionTreeProps { section: string; - docPaths: string[]; + docs: { slug: string; title: string }[]; pathname: string; } -function SectionTree({ section, docPaths, pathname }: SectionTreeProps) { - const tree = buildFolderTree(docPaths.filter((p) => p.startsWith(`${section}/`)), section); +function SectionTree({ section, docs, pathname }: SectionTreeProps) { + const tree = buildFolderTree( + docs.filter((d) => d.slug.startsWith(`${section}/`)), + section, + ); const sectionInfo = SECTIONS.find((s) => s.id === section); if (!sectionInfo) return null; @@ -123,7 +142,6 @@ function SectionTree({ section, docPaths, pathname }: SectionTreeProps) { export default async function HomePage() { const all = await listDocuments(); - const docPaths = all.map((d) => d.path); const recent = [...all] .sort( @@ -194,7 +212,7 @@ export default async function HomePage() { {/* Inline folder tree */} ({ slug: d.slug, title: d.title }))} pathname="/" />