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
+61
View File
@@ -304,3 +304,64 @@ export async function deleteDocument(slug: string): Promise<{ deleted: boolean }
return { deleted: true };
}
// ---------------------------------------------------------------------------
// Phase 14: Hierarchical folder structure helpers.
// These enable GitHub-style nested folder browsing — the content creator
// decides folder structure via where they place files; no config needed.
// ---------------------------------------------------------------------------
/**
* Extract the folder structure for a given section from a list of document paths.
* Returns all distinct folder prefixes (relative to the section), sorted.
*
* Example: for section "docs" with paths ["docs/a/b/c.md", "docs/a/b/d.md"],
* returns ["a", "a/b"].
*/
export function extractFoldersForSection(
docPaths: string[],
section: string,
): string[] {
const folders = new Set<string>();
const base = `${section}/`;
for (const path of docPaths) {
const rel = path.startsWith(base) ? path.slice(base.length) : path;
const cleanPath = rel.replace(/\.md$/, "");
const parts = cleanPath.split("/");
let acc = "";
for (let i = 0; i < parts.length - 1; i++) {
acc = acc ? `${acc}/${parts[i]}` : parts[i];
folders.add(acc);
}
}
return [...folders].sort();
}
/**
* Given a full URL slug path (e.g. "writeups/ctf/defcon-quals-2024"), determine
* if it represents a folder (i.e., there are other docs whose paths start with
* this prefix) or a leaf document.
*
* Returns:
* - "doc" if the path is a leaf document (path + ".md" matches a doc)
* - "folder" if the path is a parent of other doc paths
* - "none" if neither
*/
export function classifyPath(
docPaths: string[],
path: string,
): "doc" | "folder" | "none" {
const dotMd = `${path}.md`;
// Check if it's a leaf document
if (docPaths.includes(dotMd)) return "doc";
// Check if it's a folder (parent of other paths)
const prefix = `${path}/`;
const hasChildren = docPaths.some((p) => p.startsWith(prefix));
if (hasChildren) return "folder";
return "none";
}