feat(core): dynamic custom frontmatter fields for CTF writeup metadata
CI / typecheck + build (turbo) (push) Canceled after 0s
CI / typecheck + build (turbo) (push) Canceled after 0s
- Add extra_fields JSONB column to documents table (migration 0003) - CreateDocInput/UpdateDocInput: extraFields?: Record<string, string> - parseFile: extracts non-standard frontmatter keys as extraFields - stringifyFile: writes extraFields dynamically to YAML frontmatter - toMeta: spreads extraFields from DB row into DocumentMeta - DocForm: '+ Add field' UI for content creators to add any metadata - API routes: splitPayload() separates standard vs custom fields - Doc page: CustomFieldBadges dynamically renders any custom field - Add db:migrate / db:push scripts + deploy workflow migration step - Add CTF writeup template (content/writeups/ctf/template/) - Add sample DEF CON writeup with event/challenge/category/difficulty/points - Add @tailwindcss/typography for table rendering (prose classes) - Add remark-gfm for GitHub Flavored Markdown table parsing
This commit is contained in:
@@ -48,6 +48,20 @@ 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",
|
||||
]);
|
||||
const extraFields: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
if (!STANDARD_FRONTMATTER_KEYS.has(k) && v !== undefined && v !== null) {
|
||||
extraFields[k] = String(v);
|
||||
}
|
||||
}
|
||||
|
||||
const meta: DocumentMeta = {
|
||||
id: slug,
|
||||
slug,
|
||||
@@ -60,6 +74,18 @@ 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,
|
||||
};
|
||||
|
||||
return { meta, body: content };
|
||||
@@ -92,6 +118,14 @@ 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)
|
||||
...(meta.extraFields ?? {}),
|
||||
};
|
||||
const yaml = "---\n" +
|
||||
Object.entries(data)
|
||||
|
||||
Reference in New Issue
Block a user