CMS-krønike·marts 2026·

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:

  1. Tilpassede HTML-tags (<div data-interactive-embed>) — escapet af markdown-parseren
  2. html: true i markdown-it — brød eksisterende indhold
  3. editor.getHTML() i stedet for getMarkdown() — brød hele arkitekturen (indhold ≠ præsentation)
  4. HTML-kommentarer (<!-- interactive:id -->) — fjernet af markdown-it med html: false
  5. 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:

![photo](url "float:left|width:300px")

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.prompt med 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.