CMS Chronicle #11: Interaktive elementer, medieadaptere og Markdown-spørgsmålet
Vi udviklede en hel interaktive motor, et pluggable medieadapter-system, GitHub-native mediehåndtering og stod over for det sværeste arkitekturspørgsmål hidtil: Hvordan integrerer du rigt indhold i markdown uden at bryde adskillelsen mellem indhold og præsentation?
Den natlige sprint
Denne kronik dækker, hvad der måske var den mest intense 12-timers udviklingssession i projektets historie. Vi leverede en hel funktionskategori — Interaktive elementer — fra nul til produktion, genopbyggede mediesystemet fra bunden og oplevede en reel arkitektonisk krise, der tvang os til at genoverveje, hvordan rigt tekstindhold fungerer.
Interaktive motor
Interaktive elementer — eller blot "Ints" som vi nu kalder dem — er selvstændige interaktive HTML-komponenter: Chart.js-visualiseringer, animerede diagrammer, priskalkulatorer, interaktive demos. De lever i deres egen manager, har deres egen editor og kan integreres i indhold.
Hvad vi byggede på en enkelt session:
- Interaktive manager med gitter-/listevisninger, søgning, miniaturer (Pitch Vault-stil)
- Tre redigeringsmodi: Forhåndsvisning (iframe), Visuel redigering (contentEditable-injektion fra Pitch Vault), Kode (Monaco-editor med HTML-syntaksfremhævning)
- Fuld statusworkflow: kladde → publiceret → slettet, der matcher dokumenteditoren præcist
- Egenskabspanel med omdøbning, ID, filnavn, datoer
- Klon, Historik, Slet — alle matcher dokumenteditorens knaplayout og ikonstørrelser
- TipTap-embed-node — indsæt Ints i rigt tekstindhold via en vælger
Detaljesiden matcher dokumenteditoren pixel-for-pixel: samme 48px fastklæbende topbjælke, samme knapkomponenter, samme lodrette separator, samme ikonstørrelser. Konsistens betyder noget.
MedieAdapter-mønsteret
Den største arkitektoniske tilføjelse: et pluggable MedieAdapter-interface, der abstraherer alle medieoperationer.
interface MediaAdapter {
listMedia(): Promise<MediaFileInfo[]>;
uploadFile(filename: string, content: Buffer, folder?: string): Promise<{ url: string }>;
trashFile(folder: string, name: string): Promise<void>;
restoreFile(folder: string, name: string): Promise<void>;
readFile(pathSegments: string[]): Promise<Buffer | null>;
// ... plus al CRUD for interaktive elementer
}
To implementeringer leveres i dag:
- FilesystemMediaAdapter — læser/skriver til lokal disk
- GitHubMediaAdapter — læser/skriver via GitHub Contents API
Hver API-rute kalder getMediaAdapter() og fungerer identisk uanset backend. At tilføje Supabase Storage er blot en ny klasse.
GitHub som lager: Virkeligheden
Vi bruger GitHub som en fuld lageradapter — dokumenter OG medier. Filer lever i repoet, hver CMS-gem er en commit. Men at vise GitHub-værtsede billeder i admin-UI’en afslørede en overraskende udfordring: raw.githubusercontent.com leverer ALLE filer som text/plain med X-Content-Type-Options: nosniff. Browsere nægter at gengive dem som billeder.
Vores løsning: Next.js-omskrivninger i next.config.ts, der videresender /images/*, /audio/* og /interactives/* gennem CMS-adminens eget /api/uploads/ endpoint, som henter fra GitHub med korrekte MIME-typer. Til produktionssites leverer previewUrl-indstillingen medier direkte fra det deployede site — ingen videresendelse nødvendig.
Markdown-krisen
Det sværeste øjeblik i sessionen. Vi skulle integrere filvedhæftninger og interaktive elementer i rigt tekstindhold. Den rigt teksteditor gemmer markdown. Markdown kender ikke til tilpassede indlejrede noder.
Vi prøvede alt:
- Tilpassede HTML-tags (
<div data-interactive-embed>) — escapet af markdown-parseren html: truei markdown-it — brød eksisterende indholdeditor.getHTML()i stedet forgetMarkdown()— brød hele arkitekturen (indhold ≠ præsentation)- HTML-kommentarer (
<!-- interactive:id -->) — fjernet af markdown-it medhtml: false - Teksttokens (
!!INTERACTIVE[id|title]) — overlevede tur-retur men krævede sidespecifik parsing
Hvert tilgang løste én ting og brød en anden. Det var ægte demoraliserende.
Løsningen: Blokke
Svaret lå allerede i kodebasen. Vi havde bygget et bloksystem for måneder siden. Løsningen:
Integrer ikke komplekst indhold i markdown. Brug blokke.
En blogindlæggets sections-felt er et blok-array:
{ name: "sections", type: "blocks", blocks: ["text", "interactive", "image", "file"] }
Hver bloktype håndterer sin egen gengivelse:
- text → ren markdown, gengivet med
react-markdown - interactive → reference til en Int, gengivet som iframe
- image → billede med alt-tekst og billedtekst
- file → downloadlink
Markdown forbliver ren. Ingen tokens, ingen hack, ingen escapet HTML. Det rigt tekstfelt gør, hvad det er godt til — tekstformatering. Alt andet er en blok.
Læringen om gengivelse
Vi lærte også, at det rigt tekst-gengiveren betyder enormt. En regex-baseret renderMarkdown()-funktion kan ikke håndtere billeder med størrelse, tabeller eller noget ikke-trivielt markdown.
Den korrekte tilgang: react-markdown med remark-gfm og tilpassede komponenter. Specifikt skal img-komponenten parse TipTaps titel-felt for float- og width-information:

Dette er nu dokumenteret i CLAUDE.md som standardmønsteret, og vi bygger en Next.js-skabelon, der har det hele korrekt opsat.
Alt andet
- Medie-manager: dynamiske typefiltre (Billeder, SVG, Lyd, Dokumenter, Interaktive elementer), korrekte ikoner pr. type, brugssporing på tværs af alle medie-stier
- Blød sletning overalt: mediefiler, Ints og dokumenter går alle til papirkurven med advarselsdialoger
- Billedfelt: upload + Gennemse medie-modal med søgning
- Filvedhæftning i editor: upload + Gennemse medie
- Site-omskifter-synkronisering: at klikke på et site i dashboardet synkroniserer nu det øverste dropdown
- Indholds-sidebar: husker åben/lukket tilstand i localStorage
- Alle modaler lukker på Escape: gennemgik hele kodebasen
- Alle slettehandlinger har bekræftelse: inline "Sikker?" eller modal-dialog
- Video-URL-dialog: erstattede native
window.promptmed en tilpasset stylet dialog - AI-bubble-menu: skjult for ikke-tekstnoder (Ints, lyd, video, filvedhæftninger)
- Fix af preview-URL: indeholder ikke længere kategori i stien
- Træk-og-slip fanebladsordning — planlagt
- GitHub-site-auto-synkronisering & webhook-revalidering — planlagt
- Framework-skabeloner — planlagt
Hvad kommer næste gang
Skabelonen er den umiddelbare prioritet. I stedet for 2000 linjer CLAUDE.md-instruktioner, vil AI-sitebyggere klone et fungerende Next.js-projekt med alle mønstre allerede implementeret.
Derefter: webhook-revalidering, så GitHub-sites opdateres automatisk, AI-redigeringsmodus for Ints og den datadrevne arkitektur, hvor interaktive elementer læser deres indhold fra CMS-samlinger.
CMS’en er ikke død. Den udvikler sig til noget, ingen traditionel CMS har været — en AI-native indholdsplatform, hvor grænserne mellem strukturerede data, rigt tekst og interaktive oplevelser er flydende, ikke faste.