Static prose is no longer the ceiling of a content management system. Modern CMSs are expected to let editors place embeds, interactive components, charts, calculators, and full scrollytelling narratives directly into the content flow — and to do so without handing an attacker a script-injection vector or shipping a megabyte of JavaScript to every reader. This chapter maps how the leading content models (block-based, Portable Text, rich-text-with-embeds, and MDX) expose interactivity to editors; how islands architecture and selective hydration keep that interactivity cheap; how component registries (notably the shadcn/AI-agent pattern) are reshaping where components come from; and how embeds are sandboxed and sanitized so "rich" does not become "remotely exploitable." It is deliberately stack-agnostic: the same four or five patterns recur whether you are in WordPress, Sanity, Storyblok, Contentful, or a Git-backed MDX repo.
Every CMS has to reconcile two opposing pressures. Editors want to drop a poll, a stock-price calculator, a 3D model, or a New York Times-style scroll-driven map into the middle of an article. Developers want content to stay structured, queryable, portable, and safe — not a soup of raw <script> tags. The history of "rich content" is the history of CMSs finding a typed slot for interactivity: a named, schema-defined object that the editor can place and configure, but whose actual rendering is owned by code.
There are four dominant representations of in-content interactivity in 2026:
| Model | Representation | How interactivity is placed | Lead platforms |
|---|---|---|---|
| Block model | Tree of typed blocks (often JSON or serialized HTML) | Editor inserts a registered block type, fills its attributes | WordPress (Gutenberg), Storyblok, Webflow, Builder.io |
| Portable Text | JSON array of spans + custom inline/block objects | Editor inserts a custom object type registered in the editor's of array | Sanity |
| Rich text + embedded entries |
| Structured rich-text JSON with references to other entries |
| Editor embeds a content entry as a block or inline node |
| Contentful, Hygraph |
| MDX | Markdown + JSX components | Author writes (or visually inserts) a <Component/> tag | TinaCMS, Astro/Next content collections, Git-backed sites |
The crucial design insight shared across all four: the interactive thing is a data node, not markup. The editor manipulates structured data ("a Chart block with dataset=Q3-sales and type=bar"); the front end maps that node to a component at render time. This decoupling is what makes embeds portable, themeable, accessible, and — most importantly — sanitizable.
WordPress's block editor (Gutenberg) is the highest-volume example of editor-placed interactivity, and its 2024–2026 evolution is instructive. Two block flavours coexist:
render_callback on each request — required for query loops, frequently-changing data, and any block that uses the Interactivity API (per the WordPress Block Editor Handbook).The Interactivity API, introduced in WordPress 6.5 and standardized in 6.6, is the substantive 2025–2026 story. It gives block authors a declarative, directive-based way to add front-end behaviour (counters, popups, instant search, carts, client-side navigation) without hand-rolling a JS bundle per block. Critically, the runtime only hydrates interactive elements and uses DOM diffing and batched updates, keeping static content static — the same selective-hydration philosophy that islands frameworks pioneered, now baked into the world's most-used CMS. The 2025 Block Bindings API complements this by wiring block attributes to dynamic sources (custom fields, post meta, external APIs), so an editor-placed block can show live data without bespoke code. WordPress's experimental Telex (announced at WordCamp US 2025) pushes further: describe a block in natural language and get a generated, downloadable plugin — an explicit on-ramp from AI prompt to placeable interactive block.
Headless visual builders generalize the same pattern. Storyblok's core abstraction is the block — a reusable component with a defined schema; developers define blocks, editors assemble pages visually in the Visual Editor, with clicking a block scrolling to its rendered element and live two-way preview. For genuinely interactive embeds, Storyblok documents an "embedded blocks" pattern that integrates external interactive experiences (e.g., via StackBlitz) into a component. Builder.io and Webflow follow the register-component-then-drag model, where the developer exposes a React/Vue component (with typed inputs) into the visual editor's insert menu.
Sanity's Portable Text treats rich text as a JSON array rather than HTML, which makes custom interactivity unusually clean. To add, say, a YouTube embed or an interactive chart, a developer:
youtube type with a url field, or a chartBlock with dataset references).of array of the Portable Text field, so it appears in the editor's insert menu.@portabletext/react (the successor to the deprecated @sanity/block-content-to-react), mapping each custom type to a React component.Because the content is data, the same Portable Text array can be rendered to React, Vue, native mobile, or even read aloud — and an embed that the front end doesn't recognize simply falls through to a default renderer instead of injecting raw HTML. This is the model's security and portability payoff: there is no arbitrary HTML to sanitize because there is no arbitrary HTML, only typed nodes.
Contentful and Hygraph occupy a middle ground: a structured rich-text JSON document that can reference other content entries inline. Contentful's Rich Text field supports three ways to bring in another content type — embedded block entry, embedded inline entry, and linked (hyperlink) entry — and the front end renders each via a node-type-to-component map (Contentful's @contentful/rich-text-react-renderer). Hygraph similarly distinguishes block embeds (returned as a <div>), inline embeds (a <span> with data-gcms-embed-inline), and link embeds (an <a> with data-gcms-embed-id/data-gcms-embed-type), per Hygraph's docs. The practical effect for editors is identical to Portable Text: insert a reference to a "Product Callout" or "Interactive Map" entry, and the developer's renderer decides how that becomes interactive. The advantage over MDX is that the embedded thing is itself a reusable, separately-editable, separately-localizable content entry.
MDX (Markdown + JSX) is the developer-favoured model and the one most aligned with the AI-native, Git-backed wing of the CMS landscape. Authors write Markdown and drop in <Chart/>, <Tabs/>, <Callout/>, <Mermaid/>, or <PricingCalculator/> components inline. The benefits cited across MDX tooling (MDX project, TinaCMS, content-collections): type-safety with build-time errors, Git workflow and review, full React capability, zero-latency static rendering, and no SaaS fee.
The historical weakness of MDX — that raw JSX is hostile to non-technical editors and dangerous if untrusted users can author it — is being addressed from two directions:
components map passed to the MDX provider is an allow-list. A component the author references that isn't registered simply doesn't render — there is no path to execute arbitrary code that the developer didn't explicitly expose. MDX from untrusted sources, however, must never be evaluated directly; it is full JavaScript and should be treated like running submitted code.The cost of rich content is JavaScript. The dominant 2026 answer is islands architecture (Astro's term, but the pattern is now everywhere): render the page to fast static HTML, then add small "islands" of JS only where interactivity or personalization is needed. Astro documents two flavours:
Astro's five client:* directives let an editor's embed declare when it costs anything:
| Directive | Hydrates | Typical use for an embed |
|---|---|---|
client:load | Immediately on page load | Above-the-fold calculator the reader uses at once |
client:idle | When the main thread is idle | Non-urgent widget |
client:visible | When it scrolls into the viewport | A chart or map far down a long article |
client:media | When a media query matches | Mobile-only or desktop-only interactivity |
client:only | Client-only, never server-rendered | Embeds that can't SSR (some 3rd-party widgets) |
Astro's own framing — a typical docs site ships 80%+ less JavaScript than the Next.js/Nuxt equivalent — captures why this matters for content sites stuffed with embeds: ten interactive components no longer means ten eagerly-hydrated bundles. client:visible on data-viz embeds is the single highest-leverage performance lever for article-heavy CMSs. The same idea now appears as Gutenberg's Interactivity API (hydrate only interactive elements), React Server Components + selective 'use client' boundaries in Next.js, Qwik's resumability (skip hydration almost entirely), and Deno/Fresh islands. For a CMS, the practical guidance is: map each editor-placeable interactive block to a lazy/visible hydration boundary by default, and let only the rare above-the-fold widget opt into eager loading.
Where do these placeable components come from? The 2025 inflection point was shadcn/ui's CLI 3.0 and registry MCP server (shadcn/ui changelog, August 2025). Rather than installing components from npm as opaque packages, the shadcn model distributes components as source you own, fetched from a registry declared in components.json. CLI 3.0 added namespaced registries (@registry/name syntax), private/authenticated registries (bearer token, API key, custom headers), and cross-registry dependency resolution.
The MCP angle is what makes this AI-native: the registry MCP server lets an AI assistant browse, search, and install registry components by natural language ("add a login form from the shadcn registry," "build a landing page using components from the acme registry"). shadcn's pitch — that this "cuts out the cycle of AI-generated code that looks right but fails at runtime" — is precisely the failure mode AI coding agents hit: hallucinated props and imports. A typed registry plus MCP gives the agent a real, installable, version-pinned catalog. For a CMS team, this points at a future where the set of interactive blocks an editor can place is itself an MCP-addressable registry that both humans and agents draw from, with the same allow-list discipline applied to AI-suggested embeds as to human-authored ones.
For charts, two patterns dominate, and CMSs usually support both:
<script>) that pastes anywhere HTML is accepted. Datawrapper originated as a journalist charting tool and emphasizes fine-grained, automatically-responsive charts; Flourish leans into animated, creative chart types and has built-in scrollytelling. Both auto-resize across devices. The CMS treats the embed as an "external embed" block (see oEmbed below). Upside: editors are fully self-serve and the viz is sandboxed in an iframe. Downside: an extra network round-trip and a hard boundary the page can't style.<Chart/> block (often wrapping D3, Chart.js, Recharts, or Observable Plot) whose data comes from a CMS field, an external API via Block Bindings, or a referenced dataset entry. Upside: themed, accessible, SSR-able, no third-party iframe. Downside: developer must build and maintain it. Calculators (mortgage, pricing, ROI, dosage) are almost always this native-component path because their logic is bespoke; the editor places the calculator block and configures parameters (rates, labels, default values) as typed fields.Scroll-driven storytelling — the NYT/Pudding/National Geographic genre where graphics transform as the reader scrolls — is the most demanding rich-content form. The de-facto open-source engine is Scrollama (current 3.x), a lightweight library built on IntersectionObserver with React and Vue integrations, typically paired with D3 for the graphics. Lower-code routes have proliferated: Flourish's paid scrollytelling, and Closeread, a Quarto extension for scroll-based narratives. In a CMS, scrollytelling is best modeled as a structured sequence: a parent "Scrolly" block containing an ordered list of "step" sub-blocks (each with prose + a state to apply to a sticky graphic). This keeps the narrative editable, reorderable, and translatable rather than locked in hand-written markup — the same structured-content discipline applied to the hardest case. Maglr's 2026 roundup and the EU data-visualisation guide both stress accessibility and reduced-motion fallbacks, which a structured model makes enforceable (a no-scroll linear fallback rendered from the same step data).
Rich, embeddable content is a classic XSS surface, and 2025 produced a textbook case: Silverstripe CMS's January 2025 patches addressed an oEmbed flaw where unsanitized HTML returned by an oEmbed provider could execute XSS on both the CMS and the front end. The fix — and the prevailing best practice — is iframe sandboxing: wrap untrusted embed HTML in an <iframe> (ideally srcdoc, served from a different origin) with a restrictive sandbox attribute, with an allow-list of trusted domains that may bypass sandboxing.
The defensive toolkit for editor-placeable embeds:
| Control | What it does | Note |
|---|---|---|
sandbox attribute | Strips a frame's privileges (forms, scripts, same-origin, popups) unless re-granted token-by-token | Most critical iframe control; grant the minimum (e.g., allow-scripts without allow-same-origin) |
| Different-origin iframe | Embed HTML can't read the parent's cookies/DOM | oEmbed best practice: render provider HTML in a cross-origin frame |
credentialless | Loads cross-origin frames without sending cookies/auth tokens | Newer browser control for sensitive contexts |
| DOMPurify / server-side sanitizer | Strips scripts/event handlers from rich-text HTML before storage/render | For any HTML that isn't iframed |
| Content Security Policy (CSP) | Restricts what scripts/frames may load and execute | Defense-in-depth; constrain frame-src, script-src |
| oEmbed provider allow-list | Only fetch/trust embeds from vetted providers (YouTube, Vimeo, etc.) | Don't honor arbitrary oEmbed endpoints |
| Typed-node rendering | Render structured nodes to known components, never dangerouslySetInnerHTML | The Portable Text / block-model advantage |
The strategic takeaway: the structured-node CMSs (Sanity Portable Text, the block model, embedded entries) have a built-in security edge because editors place typed objects, not HTML, so there's nothing to inject. The risk concentrates in (a) oEmbed/raw-HTML embed blocks, which must be iframe-sandboxed from a separate origin, and (b) any path where untrusted users author MDX or HTML, which must be sanitized (DOMPurify) or — for MDX — never evaluated at all. WordPress's own performance/security discussions (e.g., wrapping embeds in srcdoc iframes) reflect the same convergence.
Two threads from elsewhere in this report intersect here. First, llms.txt and AI crawlers: interactive embeds rendered only client-side (an iframe widget, a client:only island) are invisible to LLM crawlers and to the increasing share of readers arriving via AI summaries. Best practice is graceful degradation — a server-rendered fallback (the chart's underlying table, the calculator's explanatory prose) that survives JS-off and bot consumption. Second, agentic authoring: as AI assistants gain MCP access to component registries (shadcn pattern) and to the CMS itself, the allow-list of placeable interactive blocks becomes the governance boundary. An agent that can place embeds should be constrained to the same typed, sandboxed registry a human editor uses — the agent is a new "editor" subject to the same component allow-list and embed sanitization, not an exception to it.
client:visible (lazy-hydrate data-viz when scrolled into view) is the single highest-leverage performance lever for article-heavy sites; map each placeable block to a lazy hydration boundary by default.render_callback/Interactivity API are required.client:* directives, and the 80%-less-JS claim.of array, and @portabletext/react serializers.components allow-list provider model.sandbox attribute as the critical iframe control; isolation best practices.data-gcms-embed-*).