Build a Headless Site with dotCMS
Enable Visual Editing
In this chapter you'll connect your site to dotCMS's Universal Visual Editor (UVE). By the end, content authors can click into any page on your running site and edit content in context — no switching between the frontend and dotCMS admin.
What UVE is#
UVE turns your Next.js pages into editable surfaces. When an author opens a page through the dotCMS editor, your app detects it's in edit mode and activates edit overlays — click a banner to edit its title, drag a blog card to a different container, add a new contentlet without touching code.
This works on localhost. You don't need a deployed site.
Step 1: Configure dotCMS#
In dotCMS admin, you need to register your Next.js app as a UVE app so dotCMS knows where to load it.
- Go to Settings → Apps → Universal Visual Editor
- Click the row for your site (
bank.com) - In the configuration field, paste this JSON:
{ "config": [ { "pattern": ".*", "url": "http://localhost:3000", "options": { "allowedDevURLs": ["http://localhost:3000"] } } ] }
The pattern is a regex that matches which URL paths this config applies to — .* matches everything. The url is where dotCMS loads your frontend inside the editor iframe. allowedDevURLs whitelists localhost so the browser doesn't block the iframe connection.
Step 2: Add useEditableDotCMSPage to your views#
useEditableDotCMSPage is a hook from @dotcms/react that does two things: it listens for edit-mode signals from the dotCMS editor iframe, and it returns a live pageAsset that updates as the author makes changes.
You need to add it to every view that uses DotCMSLayoutBody.
Update Page.tsx#
Open src/views/Page.tsx and update it:
"use client"; import { DotCMSLayoutBody, useEditableDotCMSPage } from "@dotcms/react"; import type { DotCMSComposedPageResponse, DotCMSPageResponse } from "@dotcms/types"; import { pageComponents } from "@/components/content-types"; interface PageProps { pageContent: DotCMSComposedPageResponse<DotCMSPageResponse>; } export function Page({ pageContent }: PageProps) { const { pageAsset } = useEditableDotCMSPage(pageContent); return ( <main className="page-main"> <DotCMSLayoutBody page={pageAsset} components={pageComponents} /> </main> ); }
Update BlogListingPage.tsx#
Open src/views/BlogListingPage.tsx and make the same changes:
"use client"; import { ReactNode } from "react"; import { DotCMSLayoutBody, useEditableDotCMSPage } from "@dotcms/react"; import type { DotCMSComposedPageResponse, DotCMSPageResponse } from "@dotcms/types"; import { pageComponents } from "@/components/content-types"; interface BlogListingPageProps { pageContent: DotCMSComposedPageResponse<DotCMSPageResponse>; slots?: Record<string, ReactNode>; } export function BlogListingPage({ pageContent, slots }: BlogListingPageProps) { const { pageAsset } = useEditableDotCMSPage(pageContent); return ( <main className="page-main"> <DotCMSLayoutBody page={pageAsset} components={pageComponents} slots={slots} /> </main> ); }
Update DetailPage.tsx#
Open src/views/DetailPage.tsx and add the hook:
"use client"; import DotCMSImage from "@/components/DotCMSImage"; import { DotCMSBlockEditorRenderer, useEditableDotCMSPage } from "@dotcms/react"; import type { DotCMSComposedPageResponse, DotCMSPageResponse } from "@dotcms/types"; import type { BlogURLContentMap } from "@/types/blog"; interface DetailPageProps { pageContent: DotCMSComposedPageResponse<DotCMSPageResponse>; } export function DetailPage({ pageContent }: DetailPageProps) { const { pageAsset } = useEditableDotCMSPage(pageContent); const { title, image, body, publishDate } = (pageAsset?.urlContentMap as BlogURLContentMap) ?? {}; return ( <main className="detail-page"> <article> {title && <h1>{title}</h1>} {publishDate && ( <time className="detail-page__date"> {new Date(publishDate).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric", })} </time> )} {image && ( <div className="detail-page__image"> <DotCMSImage src={image} width={800} height={400} alt={title || ""} /> </div> )} {body?.json && ( <DotCMSBlockEditorRenderer blocks={body.json} className="detail-page__body" /> )} </article> </main> ); }
Try it#
Open http://localhost:8082/dotAdmin/#/edit-page/content?url=%2Findex — your home page loads inside the dotCMS editor. Click the banner, edit the title, and watch it update live. Then try /blog and a blog post the same way.
This is the payoff of the slots pattern from Chapter 4: the author changes the BlogList widget config and the listing updates immediately — no code change, no refresh.
Checkpoint#
- UVE app configured in dotAdmin pointing to
http://localhost:3000 - Opening a page in dotAdmin loads it inside the editor
- Clicking a contentlet opens the edit panel
- Saving a change reflects on the page without a full reload
Next up
Chapter 7: Add Page Metadata