feat: dynamic custom frontmatter fields + CTF writeup template system
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<string, string> - 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
This commit is contained in:
@@ -48,6 +48,7 @@ jobs:
|
|||||||
/home/code/.bun/bin/bun install --frozen-lockfile
|
/home/code/.bun/bin/bun install --frozen-lockfile
|
||||||
/home/code/.bun/bin/bun --cwd apps/web run build
|
/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 --cwd packages/db run db:migrate
|
||||||
|
/home/code/.bun/bin/bun run index
|
||||||
sudo systemctl restart mcpedia-web mcpedia-api mcpedia-mcp mcpedia-worker
|
sudo systemctl restart mcpedia-web mcpedia-api mcpedia-mcp mcpedia-worker
|
||||||
sleep 3
|
sleep 3
|
||||||
systemctl --no-pager status mcpedia-web mcpedia-api mcpedia-mcp mcpedia-worker --no-legend
|
systemctl --no-pager status mcpedia-web mcpedia-api mcpedia-mcp mcpedia-worker --no-legend
|
||||||
|
|||||||
@@ -17,6 +17,16 @@ const VALID_TYPES: DocType[] = [
|
|||||||
"note",
|
"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 {
|
export interface ParsedFile {
|
||||||
meta: DocumentMeta;
|
meta: DocumentMeta;
|
||||||
body: string;
|
body: string;
|
||||||
@@ -48,17 +58,13 @@ export function parseFile(absPath: string, relPath: string): ParsedFile {
|
|||||||
const createdAt = data.created_at ?? nowIso;
|
const createdAt = data.created_at ?? nowIso;
|
||||||
const updatedAt = data.updated_at ?? data.created_at ?? nowIso;
|
const updatedAt = data.updated_at ?? data.created_at ?? nowIso;
|
||||||
|
|
||||||
// Extract any additional frontmatter keys as dynamic extra fields.
|
// Extract any non-standard frontmatter keys as dynamic extra fields.
|
||||||
// These are written to DB as JSONB + rendered as dynamic badges in the UI.
|
// These are stored in 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",
|
|
||||||
]);
|
|
||||||
const extraFields: Record<string, string> = {};
|
const extraFields: Record<string, string> = {};
|
||||||
for (const [k, v] of Object.entries(data)) {
|
for (const [k, v] of Object.entries(data)) {
|
||||||
if (!STANDARD_FRONTMATTER_KEYS.has(k) && v !== undefined && v !== null) {
|
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,
|
path: relPath,
|
||||||
createdAt: String(createdAt),
|
createdAt: String(createdAt),
|
||||||
updatedAt: String(updatedAt),
|
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,
|
extraFields,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,13 +113,11 @@ export function stringifyFile(
|
|||||||
path: relPath,
|
path: relPath,
|
||||||
created_at: meta.createdAt,
|
created_at: meta.createdAt,
|
||||||
updated_at: meta.updatedAt,
|
updated_at: meta.updatedAt,
|
||||||
// CTF writeup metadata (only written if present)
|
// Dynamic custom fields — any key the content creator added.
|
||||||
...(meta.event && { event: meta.event }),
|
// These are written as-is to YAML frontmatter, and read back via parseFile
|
||||||
...(meta.challenge && { challenge: meta.challenge }),
|
// into meta.extraFields. Fully content-driven: no code changes needed
|
||||||
...(meta.category && { category: meta.category }),
|
// to support new metadata fields (e.g. CTF: event, challenge, category,
|
||||||
...(meta.difficulty && { difficulty: meta.difficulty }),
|
// difficulty, points, or anything else).
|
||||||
...(meta.points !== undefined && { points: meta.points }),
|
|
||||||
// Dynamic custom fields (any key the content creator added)
|
|
||||||
...(meta.extraFields ?? {}),
|
...(meta.extraFields ?? {}),
|
||||||
};
|
};
|
||||||
const yaml = "---\n" +
|
const yaml = "---\n" +
|
||||||
|
|||||||
@@ -14,14 +14,8 @@ export interface DocumentMeta {
|
|||||||
path: string; // relative path under content/
|
path: string; // relative path under content/
|
||||||
createdAt: string; // ISO
|
createdAt: string; // ISO
|
||||||
updatedAt: string; // ISO
|
updatedAt: string; // ISO
|
||||||
// CTF writeup metadata (optional — only set for writeups)
|
// Dynamic custom frontmatter fields (e.g. CTF: event, challenge, category,
|
||||||
event?: string;
|
// difficulty, points). Content creators add arbitrary key-value pairs.
|
||||||
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.
|
|
||||||
extraFields?: Record<string, string>;
|
extraFields?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user