Visual Editing
How Visual Editing Works in a Headless CMS
Understand the architecture behind visual headless CMS editing: structured content, component mapping, protected draft preview, page composition, publishing, and frontend ownership.
Traditional headless editing can feel disconnected because the CMS form and the rendered page live in different systems. An editor changes a field in one interface, opens another browser view, searches for the affected component, and repeats that loop until the page looks right.
Visual editing shortens that feedback loop, but it does not remove the headless boundary. A reliable implementation still separates structured content, page composition, protected draft delivery, frontend components, and publication.
The conceptual flow#
Structured Content -> Page Composition -> Protected Preview -> Visual Editing -> Publish -> DeliveryEach step has a different responsibility:
- Structured content defines reusable fields, entries, media, relations, and validation.
- Page composition places approved components and content references into an ordered page draft.
- Protected preview renders that draft in the actual frontend without exposing it to visitors.
- Visual editing connects selected DOM elements back to editable block placements.
- Publish validates and creates a public version.
- Delivery returns only the published version to normal website requests.
Collapsing these steps creates risk. If preview and public delivery share the same authorization or cache, unpublished content can leak. If component mapping is ambiguous, editors can select a block that the frontend cannot render consistently.
Structured content and visual composition solve different problems#
Structured content represents meaning: an Article, Product, Author, feature, location, or call-to-action configuration. It supports reuse, validation, localization, and more than one delivery channel.
Visual composition represents placement: which components appear on this page, in which region, and in what order. A component block may own its page-specific settings. A content-entry block may reference an independently managed record.
That distinction matters. Editing a reusable Article inside one page could unexpectedly change every page that uses it. Contoprix therefore treats content-entry and form blocks as placements: editors can move, duplicate, or remove the placement, while the source fields stay in their dedicated editor. Component blocks owned by the page draft can expose their fields through the visual editor.
The /visual-headless-cms page describes this product category; the architecture remains grounded in the broader headless CMS model.
Component mapping is the frontend contract#
A visual builder does not need to generate the production frontend. It needs a stable mapping between a CMS component code and application code.
import type { ComponentRegistry } from "@contoprix/react";
import HeroBanner from "@/components/contoprix/HeroBanner";
import CallToAction from "@/components/contoprix/CallToAction";
const components: ComponentRegistry = {
hero_banner: HeroBanner,
call_to_action: CallToAction,
};
export default components;The editor chooses hero_banner; the registry resolves that exact code to the application’s component. Developers still own semantic HTML, responsive layout, accessibility, performance, testing, analytics, and error states.
Contoprix PageRenderer processes the delivered block order and delegates each block to the registry. A pulled schema can provide a generic fallback for a new component type, but production designs, forms, and content-entry placements normally need intentional custom components. The Page Builder setup guide covers the current renderer and registry contract.
Why preview is usually iframe-based#
Editors need to see the real application layout, styles, fonts, responsive rules, and component behavior. Embedding a protected frontend preview inside the admin interface provides that fidelity without asking the CMS to imitate the website renderer.
The iframe is also a security boundary that must be configured deliberately:
- the CMS knows the frontend preview URL;
- the frontend authorizes the editor and fetches a draft by page ID;
- preview credentials remain on the server;
- the response is dynamic and excluded from public caching;
- browser messages are accepted only from an exact allowed admin origin;
- normal public routes continue to fetch published content.
An iframe does not make preview secure by itself. A page ID is not a secret, and an origin supplied through a URL parameter is not automatically trustworthy.
Fetch the draft on the server#
The current Next.js helper reads preview content by page ID:
import { getContoprixPreviewPage } from "@contoprix/next/server";
import { VisualPreviewCanvas } from "@/contoprix/VisualPreviewCanvas";
export const dynamic = "force-dynamic";
export default async function PreviewPage({
params,
}: {
params: Promise<{ pageId: string }>;
}) {
const { pageId } = await params;
// Authorize the editor before this SDK call.
const page = await getContoprixPreviewPage({
pageId,
languageCode: "en",
});
return <VisualPreviewCanvas initialPage={page} />;
}The example shows the Contoprix call, not an application-specific authentication system. Your route must verify the editor before fetching draft content. The credential requires preview:read and must never be placed in a NEXT_PUBLIC_ variable.
Preview data includes a versionId. Contoprix uses that version boundary for visual-editing operations and stale-version protection.
Connect rendered blocks to the editor#
PageRenderer installs the current visual-editing bridge when visual editing is enabled and the page has a preview version. Custom components must put previewAttributes on their visible outer element:
import type { ContoprixComponentProps } from "@contoprix/react";
export default function HeroBanner({
settings,
previewAttributes,
}: ContoprixComponentProps) {
const heading = String(settings?.heading ?? "");
return (
<section {...previewAttributes}>
<h2>{heading}</h2>
</section>
);
}Those attributes identify the page, draft version, block placement, and component type to the bridge. They are harmless during public delivery. In preview, they let the bridge locate the selected block in the rendered DOM.
Do not attach them to a hidden child or a tiny decorative element. Selection should match the visible component boundary an editor understands.
The message bridge has one narrow job#
The frontend bridge and the parent admin exchange selection and refresh messages. The CMS owns permissions and mutations. The frontend reports which eligible block was selected and refreshes its draft data after an authorized edit.
In the current Contoprix React renderer, the bridge:
- discovers elements carrying Contoprix preview attributes;
- reports selection to the configured parent origin;
- receives reviewed refresh messages from trusted origins;
- asks the application to refresh its preview data;
- supports placement actions such as insert, move, duplicate, and delete through the editor workflow.
The application must configure the exact admin origin. A permissive wildcard would weaken the boundary between draft content and an unrelated embedding page.
Editors and developers retain separate responsibilities#
Editors should be able to choose approved blocks, enter content, arrange placements, review language variants, and publish. They should not need to edit React or CSS to change ordinary page content.
Developers define component types with the content team, implement the registry, preserve selection attributes, handle optional data, test accessibility, and decide how delivery errors and caching work. They also control which capabilities are intentionally available as components.
The goal is not unlimited visual freedom. It is useful editorial control inside a component system the frontend team can maintain.
Publishing closes the loop#
Saving changes updates the draft. It does not update public delivery. Once the editor previews and approves the page, publication validates the draft, creates a published version, and emits a page.published webhook event.
The public frontend then retrieves the published page by path. A signed webhook or cache interval refreshes stale output. Preview and public delivery should be tested separately because a correct draft does not prove the public route, language, or cache has updated.
The publishing guide documents the current draft, publish, unpublish, and signed revalidation behavior.
Architecture checklist#
Use this checklist before enabling visual editing:
- Component codes match the frontend registry exactly.
- Every custom component spreads
previewAttributesonto its visible root. - The preview URL contains the page ID placeholder expected by the CMS.
- The application authorizes the editor before loading a draft.
- Preview credentials have
preview:readand stay server-side. - Preview responses are dynamic and never stored in public caches.
- The exact CMS admin origin is allow-listed for browser messages.
- Public routes request published content by path, not preview content by ID.
- Publishing triggers a verified cache-refresh path.
- Editors and developers agree which fields belong to page components versus reusable entries.
Visual editing works best when it preserves the headless separation instead of bypassing it. Continue with the Visual Editing documentation, Page Builder overview, and Preview guide for the concrete Contoprix setup.