feat: Phase 14 — hierarchical folder structure (GitHub-style nested folders)
CI / typecheck + build (turbo) (push) Canceled after 0s

- 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
This commit is contained in:
asepharyana
2026-08-21 08:37:41 +07:00
parent 90a43e5d0c
commit ab3a2dc456
10 changed files with 840 additions and 111 deletions
+151
View File
@@ -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 `/<section>/<slug>` 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 `<ul>` 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 `<select>` or tree picker.
### 4. Example Hierarchy
Create a real hierarchical structure to demonstrate:
```
writeups/
ctf/
defcon-quals-2024/
pwn/
pwn-100-ret2win-alignment.md
pwn-200-bof-heap.md
crypto/
crypto-100-xor.md
crypto-200-rsa.md
template/
writeup-template.md
_index.md ← folder index (optional intro)
hackthebox/
machine-name/
walkthrough.md
```
For now, reorganize the existing Defcon writeup into proper subfolders + add a
folder index page. The existing `content/writeups/ctf/defcon-quals-2024/` is
already a folder — just need the folder index route to work.
### 5. Route Changes
**Current:** `[section]/[...slug]/page.tsx` — catch-all requires ≥1 slug segment.
- `/writeups` → `notFound()` (no index for bare section unless we add one)
- `/writeups/ctf` → catch-all gets `slug=["ctf"]` → currently treated as a doc
(looks up `writeups/ctf` doc, 404 if none)
- `/writeups/ctf/defcon-quals-2024/` → catch-all `slug=["ctf","defcon-quals-2024"]`
**Plan:**
1. Add `/writeups/page.tsx` (section index) — lists top-level folders + root
docs in that section. (Currently `/docs/page.tsx` exists but `/writeups/page.tsx`
doesn't.)
2. In `[section]/[...slug]/page.tsx`: at the top of the page component, check if
the slug path is a folder (has child docs). If so, render folder index instead
of doc page.
**Section index pages:** Create `[section]/page.tsx` for all 4 sections, or a
generic one. Currently only `/docs/page.tsx` exists. Add a shared
`SectionIndex` component.
### 6. Files to Change
```
new: apps/web/app/[section]/page.tsx # generic section index (folder + doc listing)
mod: apps/web/app/[section]/[...slug]/page.tsx # add folder-index detection
mod: apps/web/app/components/Sidebar.tsx # tree from flat slugs
mod: apps/web/app/components/DocForm.tsx # parent folder picker
new: content/writeups/ctf/defcon-quals-2024/_index.md # folder intro (optional)
new: content/writeups/ctf/_index.md # CTF section intro
mod: apps/web/app/docs/page.tsx # may need generic version
```
### 7. Verification
- `/writeups` → 200, shows folders: ctf/, template/
- `/writeups/ctf` → 200, folder index listing defcon-quals-2024/
- `/writeups/ctf/defcon-quals-2024` → 200, folder index listing pwn-100-...
- `/writeups/ctf/defcon-quals-2024/pwn-100-ret2win-alignment` → 200, doc page
- Sidebar shows nested tree with folder icons
- DocForm parent folder picker works
- `bun run test` green, `turbo run typecheck` green
- Live verification via curl
## Constraints
- User: "pastikan semua dinamis dan rapih untuk banyak situasi jadi tergantung
user bukan hardcode" — folder detection must be content-driven, not config.
- No breaking existing flat slugs.
- Section icons/labels stay the same.