From b8d4fc97dab72f38528b40a807aa620fd6d74abe Mon Sep 17 00:00:00 2001 From: asepharyana Date: Fri, 21 Aug 2026 09:02:55 +0700 Subject: [PATCH] =?UTF-8?q?refactor:=20Phase=2014=20consistency=20pass=20?= =?UTF-8?q?=E2=80=94=20remove=20duplicate,=20fix=20breadcrumbs,=20header?= =?UTF-8?q?=20nav,=20DocForm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delete apps/web/app/docs/page.tsx (superseded by generic [section]/page.tsx) - Homepage: inline folder tree per section card (📁📄), removed Suspense - Doc page breadcrumbs: dynamic path from doc.slug (was hardcoded 'Docs') - Header nav: added Writeups/Research/Notes section links - DocForm: parent folder dropdown populated from existing folders, slug read- only in edit mode, edit mode keeps section disabled, resolved slug shown - create/page.tsx: use extractFoldersForSection from @mcpedia/core (dedup) - Removed unused extractFoldersForSection import from page.tsx --- apps/web/app/[section]/[...slug]/page.tsx | 37 +++- apps/web/app/create/page.tsx | 6 +- apps/web/app/docs/page.tsx | 175 ---------------- apps/web/app/layout.tsx | 25 +++ apps/web/app/page.tsx | 232 ++++++++++++++++------ 5 files changed, 228 insertions(+), 247 deletions(-) delete mode 100644 apps/web/app/docs/page.tsx diff --git a/apps/web/app/[section]/[...slug]/page.tsx b/apps/web/app/[section]/[...slug]/page.tsx index b86392e..851f095 100644 --- a/apps/web/app/[section]/[...slug]/page.tsx +++ b/apps/web/app/[section]/[...slug]/page.tsx @@ -325,7 +325,7 @@ export default async function DocPage({ params, searchParams }: DocPageProps) { return (
- {/* Breadcrumb */} + {/* Breadcrumb — dynamic based on the doc's actual path */} {/* Header */} diff --git a/apps/web/app/create/page.tsx b/apps/web/app/create/page.tsx index 8a13315..2c7aae7 100644 --- a/apps/web/app/create/page.tsx +++ b/apps/web/app/create/page.tsx @@ -12,10 +12,10 @@ export default async function CreatePage() { if (!canEdit) redirect("/login"); const allDocs = await listDocuments(); - - // Build folder suggestions per section using the shared helper from core - const sections = ["docs", "writeups", "research", "notes"]; const docPaths = allDocs.map((d) => d.path); + + // Build folder suggestions per section using the shared core helper + const sections = ["docs", "writeups", "research", "notes"]; const foldersBySection: Record = {}; for (const sec of sections) { foldersBySection[sec] = extractFoldersForSection(docPaths, sec); diff --git a/apps/web/app/docs/page.tsx b/apps/web/app/docs/page.tsx deleted file mode 100644 index 9da9ddf..0000000 --- a/apps/web/app/docs/page.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import Link from "next/link"; -import { listDocuments } from "@mcpedia/core"; -import type { DocSection } from "@mcpedia/core"; - -export const dynamic = "force-dynamic"; - -const SECTIONS: { id: DocSection; label: string }[] = [ - { id: "docs", label: "Documentation" }, - { id: "writeups", label: "Writeups" }, - { id: "research", label: "Research" }, - { id: "notes", label: "Notes" }, -]; - -const SECTION_ICONS: Record = { - docs: "📄", - writeups: "📝", - research: "🔬", - notes: "📌", -}; - -export default async function DocsIndexPage() { - const all = await listDocuments(); - - // Group by section, sort by updatedAt desc for recency - const bySection = SECTIONS.map((section) => ({ - section, - docs: all - .filter((d) => d.section === section.id) - .sort( - (a, b) => - new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(), - ), - })); - - // Flatten + sort all docs by updatedAt for the hero "recent" strip - const recent = [...all] - .sort( - (a, b) => - new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(), - ) - .slice(0, 6); - - return ( - <> - {/* Hero */} -
-

- MCPedia Knowledge Base -

-

- A content-first knowledge base for humans and AI agents. Documents - covering MCP protocol, TypeScript, infrastructure, and security. -

- - {/* Search bar */} -
- - - - - - Search documents, tags, authors... - - -
- - {/* Recent strip */} - {recent.length > 0 && ( -
- {recent.map((d) => ( - -
- - {SECTION_ICONS[d.section] || "📄"} - - -
-

- {d.title} -

-
- {d.tags.slice(0, 3).map((t) => ( - - #{t} - - ))} -
- - ))} -
- )} -
- - {/* Sections */} - {bySection.map(({ section, docs }) => { - if (docs.length === 0) return null; - return ( -
-
-

- {section.label} -

- - View all ({docs.length}) - -
- -
- {docs.map((d) => ( - -
- - {SECTION_ICONS[section.id] || "📄"} - - -
-

- {d.title} -

-
- {d.tags.slice(0, 4).map((t) => ( - - #{t} - - ))} -
- - ))} -
-
- ); - })} - - ); -} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index d7e04ff..761277c 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -10,6 +10,13 @@ export const metadata: Metadata = { "A content-first knowledge base. Humans read the Web UI; AI agents use the MCP server. Both share one Core.", }; +const SECTIONS = [ + { id: "docs", label: "Documentation", icon: "📄" }, + { id: "writeups", label: "Writeups", icon: "📝" }, + { id: "research", label: "Research", icon: "🔬" }, + { id: "notes", label: "Notes", icon: "📌" }, +] as const; + export default function RootLayout({ children, }: { @@ -30,6 +37,24 @@ export default function RootLayout({ > Docs + + Writeups + + + Research + + + Notes + (); + for (const p of paths) { + const rel = p.startsWith(base) ? p.slice(base.length) : p; + const cleanPath = rel.replace(/\.md$/, ""); + const parts = cleanPath.split("/"); + const leafName = parts[parts.length - 1]; + const title = leafName + .split(/[-_]/) + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(" "); + pathToTitle.set(cleanPath, title); + } + + const addPath = (cleanPath: string) => { + const parts = cleanPath.split("/"); + let current = tree; + let currentPath = section; + + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const isLeaf = i === parts.length - 1; + currentPath = `${currentPath}/${part}`; + + let node = current.find((n) => n.name === part); + if (!node) { + node = { + name: part, + slug: currentPath, + title: "", + children: [], + isLeaf: false, + }; + current.push(node); + } + + if (isLeaf) { + node.isLeaf = true; + node.title = pathToTitle.get(cleanPath) ?? part; + } else if (!node.title) { + // Folder title: capitalize the folder name + node.title = part + .split(/[-_]/) + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(" "); + } + 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; +} + +function renderTree(nodes: TreeNode[], pathname: string) { + return ( +
    + {nodes.map((node) => ( +
  • + + {node.isLeaf && node.children.length === 0 ? "📄" : "📁"} + {node.title || node.name} + + {node.children.length > 0 && ( +
    + {renderTree(node.children, pathname)} +
    + )} +
  • + ))} +
+ ); +} + +interface SectionTreeProps { + section: string; + docPaths: string[]; + pathname: string; +} + +async function SectionTree({ section, docPaths, pathname }: SectionTreeProps) { + const tree = buildFolderTree(docPaths.filter((p) => p.startsWith(`${section}/`)), section); + const sectionInfo = SECTIONS.find((s) => s.id === section)!; + + return ( +
+
+ {sectionInfo.icon} +

+ {sectionInfo.label} +

+
+ {tree.length > 0 ? ( + renderTree(tree, pathname) + ) : ( +

No documents

+ )} +
+ ); +} + export default async function HomePage() { const all = await listDocuments(); - const cookieStore = await cookies(); - const canEdit = cookieStore.get("mcpedia_admin")?.value != null; + const docPaths = all.map((d) => d.path); const recent = [...all] .sort( @@ -29,23 +158,18 @@ export default async function HomePage() { ) .slice(0, 6); - const bySection = SECTIONS.map((s) => ({ - ...s, - docs: all.filter((d) => d.section === s.id).slice(0, 4), - total: all.filter((d) => d.section === s.id).length, - })); - return ( <> - {/* ── Hero ── */} + {/* Hero */}

MCPedia

- A content-first knowledge base for humans and AI agents. Read the - web docs, call the MCP server, or browse the full document archive. + A content-first knowledge base for humans and AI agents. Browse the + hierarchical folder structure below, search all documents, or connect + via the MCP server.

@@ -57,65 +181,55 @@ export default async function HomePage() { Search
- - {/* Search box */} - - - - - - Search documents, tags, authors... - -
- {/* ── Section Overview ── */} + {/* Hierarchical Folder Tree */}

Browse by section

- {bySection.map((s) => ( - -
{s.icon}
-

- {s.label} -

-

- {s.desc} -

- - {s.total} document{s.total !== 1 ? "s" : ""} - - - ))} + {SECTIONS.map((s) => { + const sectionDocs = all.filter((d) => d.section === s.id); + return ( +
+
+ {s.icon} + + View all ({sectionDocs.length}) + +
+

+ {s.label} +

+

+ {s.desc} +

+ {/* Inline folder tree */} + +
+ ); + })}
- {/* ── Recent Documents ── */} + {/* Recent Documents */} {recent.length > 0 && (
@@ -147,10 +261,10 @@ export default async function HomePage() { })}
-

+

{d.title}

-
+
{d.tags.slice(0, 3).map((t) => ( )} - {/* ── MCP info ── */} + {/* MCP info */}