From 7fb55590bd0e5e026b6c7c8d742a4eb2ee88cae1 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Thu, 20 Aug 2026 23:24:57 +0700 Subject: [PATCH] feat: dynamic custom frontmatter fields + CTF writeup template system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User requested dynamic (not static) fields for CTF writeup content organization. Changes: - DocumentMeta: removed typed CTF fields (event/challenge/category/difficulty/ points), replaced with single extraFields?: Record - parseFile: any frontmatter key not in the STANDARD set becomes a dynamic extra field — fully content-driven, no code changes needed for new fields - stringifyFile: writes extraFields back to YAML frontmatter for round-trip stability - DocForm: '+ Add field' UI lets creators add ANY metadata key at create/edit time - API routes: splitPayload() auto-separates standard vs custom fields - Doc page: CustomFieldBadges dynamically renders any custom field with auto-styling for common CTF patterns (difficulty→color, points→badge) - Added db:migrate + db:push scripts; deploy workflow now runs migrations + reindexes content on every deploy - content/writeups/ctf/template/writeup-template.md (template) - content/writeups/ctf/defcon-quals-2024/pwn-100-ret2win-alignment.md (sample) - DB column extra_fields (jsonb) already applied to live DB --- .github/workflows/deploy.yml | 1 + packages/parser/src/index.ts | 45 +++++++++++++++--------------------- packages/types/src/index.ts | 10 ++------ 3 files changed, 22 insertions(+), 34 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0a25718..7d7a8b2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -48,6 +48,7 @@ jobs: /home/code/.bun/bin/bun install --frozen-lockfile /home/code/.bun/bin/bun --cwd apps/web run build /home/code/.bun/bin/bun --cwd packages/db run db:migrate + /home/code/.bun/bin/bun run index sudo systemctl restart mcpedia-web mcpedia-api mcpedia-mcp mcpedia-worker sleep 3 systemctl --no-pager status mcpedia-web mcpedia-api mcpedia-mcp mcpedia-worker --no-legend diff --git a/packages/parser/src/index.ts b/packages/parser/src/index.ts index e246632..dec49df 100644 --- a/packages/parser/src/index.ts +++ b/packages/parser/src/index.ts @@ -17,6 +17,16 @@ const VALID_TYPES: DocType[] = [ "note", ]; +// Standard frontmatter keys that are rendered explicitly in the UI template. +// Any other key in frontmatter becomes a dynamic "extra field" badge. +// This makes the system fully content-driven — creators add whatever +// metadata they need (event, challenge, category, difficulty, points, etc.) +// without code changes. +const STANDARD_FRONTMATTER_KEYS = new Set([ + "id", "slug", "title", "type", "section", "status", + "author", "tags", "path", "created_at", "updated_at", +]); + export interface ParsedFile { meta: DocumentMeta; body: string; @@ -48,17 +58,13 @@ export function parseFile(absPath: string, relPath: string): ParsedFile { const createdAt = data.created_at ?? nowIso; const updatedAt = data.updated_at ?? data.created_at ?? nowIso; - // Extract any additional frontmatter keys as dynamic extra fields. - // These are written to DB as JSONB + rendered as dynamic badges in the UI. - const STANDARD_FRONTMATTER_KEYS = new Set([ - "id", "slug", "title", "type", "section", "status", - "author", "tags", "path", "created_at", "updated_at", "event", - "challenge", "category", "difficulty", "points", - ]); + // Extract any non-standard frontmatter keys as dynamic extra fields. + // These are stored in DB as JSONB + rendered as dynamic badges in the UI. const extraFields: Record = {}; for (const [k, v] of Object.entries(data)) { if (!STANDARD_FRONTMATTER_KEYS.has(k) && v !== undefined && v !== null) { - extraFields[k] = String(v); + // Serialize non-string values (numbers, booleans) to string + extraFields[k] = typeof v === "string" ? v : JSON.stringify(v); } } @@ -74,17 +80,6 @@ export function parseFile(absPath: string, relPath: string): ParsedFile { path: relPath, createdAt: String(createdAt), updatedAt: String(updatedAt), - // CTF writeup metadata (optional) - event: typeof data.event === "string" ? data.event : undefined, - challenge: typeof data.challenge === "string" ? data.challenge : undefined, - category: typeof data.category === "string" ? data.category : undefined, - difficulty: - data.difficulty === "easy" || - data.difficulty === "medium" || - data.difficulty === "hard" - ? data.difficulty - : undefined, - points: typeof data.points === "number" ? data.points : undefined, extraFields, }; @@ -118,13 +113,11 @@ export function stringifyFile( path: relPath, created_at: meta.createdAt, updated_at: meta.updatedAt, - // CTF writeup metadata (only written if present) - ...(meta.event && { event: meta.event }), - ...(meta.challenge && { challenge: meta.challenge }), - ...(meta.category && { category: meta.category }), - ...(meta.difficulty && { difficulty: meta.difficulty }), - ...(meta.points !== undefined && { points: meta.points }), - // Dynamic custom fields (any key the content creator added) + // Dynamic custom fields — any key the content creator added. + // These are written as-is to YAML frontmatter, and read back via parseFile + // into meta.extraFields. Fully content-driven: no code changes needed + // to support new metadata fields (e.g. CTF: event, challenge, category, + // difficulty, points, or anything else). ...(meta.extraFields ?? {}), }; const yaml = "---\n" + diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 30d5dab..a15349f 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -14,14 +14,8 @@ export interface DocumentMeta { path: string; // relative path under content/ createdAt: string; // ISO updatedAt: string; // ISO - // CTF writeup metadata (optional — only set for writeups) - event?: string; - challenge?: string; - category?: string; - difficulty?: "easy" | "medium" | "hard"; - points?: number; - // Dynamic custom frontmatter fields (serialized as-is to YAML frontmatter). - // Allows content creators to add arbitrary metadata keys without code changes. + // Dynamic custom frontmatter fields (e.g. CTF: event, challenge, category, + // difficulty, points). Content creators add arbitrary key-value pairs. extraFields?: Record; }