import Link from "next/link"; import { notFound } from "next/navigation"; import { cookies } from "next/headers"; import { getDocument, getRelated, listRevisions } from "@mcpedia/core"; import { WEBHOOK_SECRET } from "@mcpedia/config"; import Markdown from "@/components/Markdown"; import DocForm from "@/components/DocForm"; import TOC from "@/components/TOC"; // Render at request time (content in Postgres, not available at build time). export const dynamic = "force-dynamic"; interface DocPageProps { params: Promise<{ section: string; slug: string[] }>; searchParams: Promise<{ edit?: string }>; } const SECTION_LABEL: Record = { docs: "Documentation", writeups: "Writeup", research: "Research", notes: "Note", }; const SECTION_ICON: Record = { docs: "๐Ÿ“„", writeups: "๐Ÿ“", research: "๐Ÿ”ฌ", notes: "๐Ÿ“Œ", }; /** * Render any extra/custom frontmatter fields as badges. * This makes the system dynamic โ€” the content creator decides what metadata * to include, not the UI template. Standard fields (title, author, tags, etc.) * are handled explicitly; any OTHER keys in frontmatter become badges here. */ function CustomFieldBadges({ doc, standardKeys, }: { doc: Record; standardKeys: Set; }) { // Convert doc to a plain record to get index signature const docRecord: Record = { ...doc } as Record; const customEntries = Object.entries(docRecord).filter( ([k, v]) => !standardKeys.has(k) && v !== undefined && v !== null && v !== "" && !k.startsWith("_"), ); if (customEntries.length === 0) return null; return (
{customEntries.map(([key, value]) => { const label = key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, " "); let colorClass = "bg-[#191a1b] border-[#23252a] text-[#d0d6e0]"; let prefix = ""; // Auto-style common CTF fields based on key name if (key === "points") { prefix = "pts"; colorClass = "bg-[#5e6ad2]/10 border-[#5e6ad2]/30 text-[#7170ff]"; } else if (key === "difficulty" || key === "level") { const v = String(value).toLowerCase(); if (v === "easy") colorClass = "bg-green-500/10 border-green-500/30 text-green-400"; else if (v === "medium") colorClass = "bg-yellow-500/10 border-yellow-500/30 text-yellow-400"; else if (v === "hard") colorClass = "bg-red-500/10 border-red-500/30 text-red-400"; else colorClass = "bg-[#191a1b] border-[#23252a] text-[#d0d6e0]"; } else if (key === "event" || key === "category") { colorClass = "bg-[#5e6ad2]/10 border-[#5e6ad2]/30 text-[#7170ff]"; } const displayValue = Array.isArray(value) ? value.join(", ") : typeof value === "object" ? JSON.stringify(value) : String(value); return ( {prefix ? `${displayValue} ${prefix}` : `${label}: ${displayValue}`} ); })}
); } export default async function DocPage({ params, searchParams }: DocPageProps) { const { section, slug } = await params; const { edit } = await searchParams; const fullSlug = `${section}/${slug.join("/")}`; const doc = await getDocument(fullSlug); if (!doc) notFound(); const cookieStore = await cookies(); const canEdit = cookieStore.get("mcpedia_admin")?.value != null; // Standard frontmatter keys that are rendered explicitly in the template. // Any other key in the document metadata becomes a dynamic badge. const STANDARD_KEYS = new Set([ "id", "slug", "title", "type", "section", "status", "author", "tags", "path", "createdAt", "updatedAt", ]); // Edit mode: inline form if (edit === "1" && canEdit) { return (
โ† Back to {doc.title}

Edit: {doc.title}

); } const related = await getRelated(fullSlug, 5); const revisions = await listRevisions(fullSlug, 10); return (
{/* Breadcrumb */} {/* Header */}
{SECTION_ICON[doc.section] || "๐Ÿ“„"} {SECTION_LABEL[doc.section] || doc.section}

{doc.title}

{doc.author || "unknown"} ยท {doc.tags.map((t) => ( #{t} ))}
{/* Dynamic custom field badges โ€” content creator controls what shows */}
{/* Content + TOC */}
{/* Related docs (card-style) */} {related.length > 0 && ( )} {/* History */} {revisions.length > 0 && ( )}
); }