From ab3a2dc456c70e11982428718f37df4f874f65c9 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Fri, 21 Aug 2026 08:37:41 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20Phase=2014=20=E2=80=94=20hierarchical?= =?UTF-8?q?=20folder=20structure=20(GitHub-style=20nested=20folders)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Section index pages ([section]/page.tsx): shows folder tree + flat doc list - Folder index pages: [section]/[...slug]/page.tsx detects folder paths and renders subfolder + document listing instead of 404 - Sidebar tree: hierarchical grouping from flat doc slugs, folder icons (๐Ÿ“) with proper indentation per depth level - DocForm v2: parent folder dropdown (populated from existing folders), slug input for leaf name, resolved path display - Core helpers: extractFoldersForSection + classifyPath exported from @mcpedia/core - CTF writeup reorganized: pwn-100 ret2win moved into pwn/ subfolder - _index.md folder intro pages for ctf/ and defcon-quals-2024/ - STANDARD_KEYS includes extraFields to avoid badge duplication --- .hermes/plans/hierarchical-folders.md | 151 ++++++++++++++ apps/web/app/[section]/[...slug]/page.tsx | 142 ++++++++++++- apps/web/app/[section]/page.tsx | 178 ++++++++++++++++ apps/web/app/components/DocForm.tsx | 115 ++++++++--- apps/web/app/components/Sidebar.tsx | 195 ++++++++++++++---- apps/web/app/create/page.tsx | 52 ++--- content/writeups/ctf/_index.md | 26 +++ .../writeups/ctf/defcon-quals-2024/_index.md | 31 +++ .../{ => pwn}/pwn-100-ret2win-alignment.md | 0 packages/core/src/document.service.ts | 61 ++++++ 10 files changed, 840 insertions(+), 111 deletions(-) create mode 100644 .hermes/plans/hierarchical-folders.md create mode 100644 apps/web/app/[section]/page.tsx create mode 100644 content/writeups/ctf/_index.md create mode 100644 content/writeups/ctf/defcon-quals-2024/_index.md rename content/writeups/ctf/defcon-quals-2024/{ => pwn}/pwn-100-ret2win-alignment.md (100%) diff --git a/.hermes/plans/hierarchical-folders.md b/.hermes/plans/hierarchical-folders.md new file mode 100644 index 0000000..eb5506e --- /dev/null +++ b/.hermes/plans/hierarchical-folders.md @@ -0,0 +1,151 @@ +# Phase 14 โ€” Hierarchical Folder Structure + +> User: "gk ada bedanya, maksud saya inginnya itu bisa yg bertingkat seperti github yg memiliki folder dalam folder" +> Context: after the full dynamic-custom-fields overhaul (Phase 13), the user +> wants document URLs/content organized in **nested folders** like GitHub โ€” +> `writeups/ctf/defcon-quals-2024/pwn-100/...` with subfolders under subfolders, +> not just one level deep. + +## Problem + +The current URL scheme is `/
/` where `slug` can contain `/` +(e.g. `writeups/ctf/defcon-quals-2024/pwn-100-ret2win-alignment` โ†’ +`/writeups/ctf/defcon-quals-2024/pwn-100-ret2win-alignment`). This works for +**files** but there are no **folder-level index pages** โ€” navigating to +`/writeups/ctf/defcon-quals-2024/` returns 404 because Next.js catch-all +`[section]/[...slug]/page.tsx` requires at least one slug segment beyond the +section, and the sidebar only shows flat doc titles (no folder tree). + +GitHub's model: `github.com/org/repo/tree/main/path/to/folder/file` โ€” every +folder has an index page (`/path/to/folder/`) listing its contents. + +## Solution + +### 1. Folder Index Pages + +**Create `apps/web/app/[section]/[...slug]/folder.tsx`** (or a parallel route). +Actually โ€” cleaner approach per Next.js App Router: the catch-all +`[section]/[...slug]/page.tsx` handles both. Add logic: if the slug resolves to +an actual markdown file โ†’ doc page (existing behavior). If the slug resolves to +a **directory** (folder of docs) โ†’ render a folder index listing all docs whose +`path` starts with that prefix. + +**Mechanism:** +- Call `listDocuments()` to get all docs. +- The incoming URL path is `{section}/{...slug}`. +- Build the "folder prefix" = `${section}/${slug.join("/")}/` (with trailing `/`, + or just `${section}/${slug.join("/")}` if no slug segments). +- Filter docs whose `doc.path` starts with that prefix. +- If exactly one doc matches AND its path === prefix (trimmed .md) โ†’ it's a + doc page (existing). If zero or multiple match and they all start with the + prefix โ†’ it's a folder index. +- Edge: a folder with exactly one doc whose path matches exactly โ€” still a doc + page. A folder is when there are docs at `prefix/sub/...`. + +**Better heuristic:** A slug path is a "folder" if there exist docs whose `path` +is `prefix/deep/...` (i.e., the slug is a parent of other doc paths, not a +leaf itself). A slug is a "leaf doc" if `path === prefix + ".md"`. + +### 2. Sidebar Tree + +**Update `Sidebar.tsx`:** +- `listDocuments()` already returns all docs with their full `slug` and `path`. +- Build a **tree** from the flat list: split each slug by `/`, create nested + folder nodes. +- Render nested `
    ` with indentation (already done via `marginLeft` based on + depth). +- **Folder nodes** (collapsed/expanded) get a folder icon ๐Ÿ“ and a CSS class. +- Clicking a folder โ†’ navigates to the folder index page `/{section}/{path}`. +- **Leaf doc nodes** โ†’ link to `/{doc.slug}` (existing behavior). +- Group by the first segment after section too (e.g. `ctf/defcon-quals-2024/` + is a folder, then `pwn-100-...` are children). + +Tree-building algorithm (from flat slugs): +``` +For slug "writeups/ctf/defcon-quals-2024/pwn-100-ret2win-alignment": + parts = ["writeups", "ctf", "defcon-quals-2024", "pwn-100-ret2win-alignment"] + โ†’ tree: writeups โ†’ ctf โ†’ defcon-quals-2024 โ†’ pwn-100-ret2win-alignment (leaf) +``` + +### 3. DocForm / Folder Selection + +**Update `DocForm.tsx`:** +- Add a "Parent folder" input (autocomplete or text) that shows existing folders + for the selected section. The slug field already supports `/` but the user + experience is better with folder picker. +- When creating, the slug becomes `{parentFolder}/{slug}` automatically. +- Show existing folder structure as ` -

    - URL-safe path under the section. -

    - -
    +
    + + +

    + Place this document inside an existing folder. The slug will be + prepended with the folder path (e.g. selecting "ctf/defcon-quals-2024" + + slug "pwn-100" โ†’ "ctf/defcon-quals-2024/pwn-100"). +

    +
    + +
    + + {isEdit ? ( + + ) : ( + setSlugInput(e.target.value)} + className={baseInputCls} + placeholder="my-document-slug" + required + /> + )} +

    + Resolved path: {effectiveSlug || section} +

    +
    +