The Universal Visual Editor from dotCMS is a framework-agnostic editor that allows full in-context editing for headless websites, single-page applications, and other remote web apps built using dotCMS as their data source.
As a React developer, you’re familiar with building dynamic web applications. Now, imagine leveraging your frontend skill with the robust headless architecture of dotCMS. This will allow you to create content-driven applications while empowering content authors to edit pages in real-time using the powerful Universal Visual Editor.
By the end of this guide, you’ll see how easy it is to use dotCMS as a backend to build a flexible and fully editable content workflow in your React App.
Why dotCMS? It’s enterprise-grade, API-first, and comes with powerful features like:
Universal Visual Editor (UVE): Edit your pages live, even with headless implementations.
Scalability: Manage multiple sites and languages effortlessly.
Flexibility: Create and reuse structured content across various platforms.
If you’re looking to build a truly modern, content-rich, and scalable application, this guide is for you.
Prerequisites Before Getting Started
Before you begin building, ensure you have the following:
dotCMS Instance 🚀 : You can use the demo instance located here.
Node.js and npm: If you don’t have these installed, follow the Node.js Installation Guide.
React App running: If you want to use a ready-to-start template check out Vite + React.
Tailwind CSS: We will use Tailwind CSS for styling. Check out the Tailwind Documentation for installation instructions.
What We’re Building
In this tutorial, we will build a simple React App that’s fully editable with dotCMS Universal Visual Editor, using dotCMS JavaScript SDK for React. Let's see the final result of the tutorial:
Note: If you get stuck at any point in this tutorial, you can find the full code in this GitHub repository: rjvelazco/dotcms-react-integration-example. You can also follow along step by step by checking out the commit history here: Commit History.
dotCMS Basic Knowledge
Note: You can skip this section if you know these concepts and are already familiar with dotCMS
dotCMS Page Structure
dotCMS has a flexible page structure consisting of the following hierarchy:
Rows → Columns → Containers → Content (e.g., news, events, blog posts).
This structure enables easy content organization within dotCMS pages.
Example Diagram:
Learn more about page structure here.
dotCMS Page API
The dotCMS Page API provides everything we need to build custom pages in a headless architecture. The response includes details about:
Layout
Containers
Page
Site
Template
This tutorial will focus on using Layout, Page, and Container.
Getting Your dotCMS Environment Ready
Create Our Example Page
Let’s create the page we’ll use in this tutorial. Follow these steps to create the page in dotCMS:
Browse to Site > Pages.
Click on Create Page and select Page Type.
Name the page example and select the System Template.
Publish the page.
Universal Visual Editor Configuration
Let’s start by configuring the Universal Visual Editor (UVE) in dotCMS for headless pages. This step is needed to use UVE to edit pages built in any JavaScript framework and served by any external hosting service. Next, let’s set up the Universal Visual Editor:
Sign in your dotCMS instance
Browse to Settings -> Apps
Select the built-in integration for UVE - Universal Visual Editor.
Select the site that will be feeding the destination pages.
In the configuration section, add your JSON object configuration. For this example, we’ll use a simple configuration to work with a localhost environment:
{
"config": [
{
"pattern": ".*",
"url": "http://localhost:3000/"
}
]
}
In this object, the key pattern is a regular expression (RegEx) that identifies to dotCMS which URLs should be associated with the React application; the URL key is the location of our React app.
Note: if you want to know more about Universal Visual Editor configuration and all you can do with it, check out our documentation: Universal Visual Editor Configuration for Headless Pages.
Building Our React + dotCMS Page
Project Folder Structure
Organize your project into the following structure:
🗂️ src
├── 🗂️ Hook
├── 🗂️ content-types
├── 🗂️ layout
│── 📄 App.jsx
└── 📄 main.jsx
Getting the Page Asset from dotCMS
The first thing we need to do is install the dotcms/client SDK. This SDK provides a method that allows us to interact with the dotCMS API.
npm i @dotcms/client
After installing the client SDK, we need to configure it to point to the dotCMS instance to retrieve the right data. We need to import the DotCmsClient SDK, and then pass our configuration to the init method :
DOTCMS_HOST: This is the URL of your dotCMS instance.
DOTCMS_AUTH_TOKEN: This is the JWT for your local dotCMS instance–If you don’t have it, you can create one following this guide: Creating an API Token in the UI.
DOTCMS_SITE_ID: The site ID from where you want to fetch the data. If it is not set, the client will use the default site–learn more about Multi-Site Management in dotCMS.
The client configuration should look like this:
import { DotCmsClient } from "@dotcms/client";
const client = DotCmsClient.init({
dotcmsUrl: `${import.meta.env.VITE_DOTCMS_HOST_KEY}`,
authToken: `${import.meta.env.VITE_DOTCMS_AUTH_TOKEN_KEY}`,
siteId: `${import.meta.env.VITE_DOTCMS_SITE_ID_KEY}`,
});
Note: We’ll be using the Vite environment variable to keep this sensitive information safe but you can use any method you like. To learn more about the Vite environment variables check out their documentation.
Let’s fetch the Page Asset
Now we’re ready to fetch the page content! 👏 Let’s use the client.page.get method, which retrieves the page content. This method accepts PageApiOptions as its parameters. In this example, we’ll pass the path and language_id.
For more details about the Page API parameters check out our documentation.
import "./App.css";
import { useEffect, useState } from "react";
import { DotCmsClient } from "@dotcms/client";
const client = DotCmsClient.init({
dotcmsUrl: `${import.meta.env.VITE_DOTCMS_HOST_KEY}`,
authToken: `${import.meta.env.VITE_DOTCMS_AUTH_TOKEN_KEY}`,
siteId: `${import.meta.env.VITE_DOTCMS_SITE_ID_KEY}`,
});
function App() {
const [pageAsset, setPageAsset] = useState(null);
useEffect(() => {
client.page
.get({ path: "/example", language_id: 1 })
.then((pageAsset) => {
console.log("DOTCMS PAGE ASSET:", pageAsset);
setPageAsset(pageAsset);
})
.catch((error) => console.log(error));
}, []);
return <>EMPTY Example</>;
}
export default App;
Now, if we look at the console, we’ll see the page response 🤩. This is awesome. With a few lines of code, we managed to get the dotCMS page 🚀.
Render Our Page with @dotcms/react
To render the page, we’ll use the @dotcms/react SDK, provided by dotCMS for React applications. This SDK handles rendering the page using the PageAsset and a map of components (we’ll define this map later in the example). First, let’s install the SDK:
npm i @dotcms/react
Once installed, let’s import the DotcmsLayout component to render our page. To make DotcmsLayout work, we need to provide the context and page path. The pageContext includes the PageAsset and the components required to render each contentType — don’t worry; we’ll dive deeper into this later.
import "./App.css";
import { useEffect, useState } from "react";
import { DotCmsClient } from "@dotcms/client";
import { DotcmsLayout } from "@dotcms/react";
const client = DotCmsClient.init({
dotcmsUrl: `${import.meta.env.VITE_DOTCMS_HOST_KEY}`,
authToken: `${import.meta.env.VITE_DOTCMS_AUTH_TOKEN_KEY}`,
siteId: `${import.meta.env.VITE_DOTCMS_SITE_ID_KEY}`,
});
function App() {
const [pageAsset, setPageAsset] = useState(null);
useEffect(() => {
client.page
.get({ path: "/example", language_id: 1 })
.then((pageAsset) => setPageAsset(pageAsset))
.catch((error) => console.log(error));
}, []);
if (!pageAsset) {
return <div>Loading...</div>;
}
return (
<div className="flex flex-col gap-6 min-h-screen bg-slate-200">
<main className="container m-auto">
<DotcmsLayout
pageContext={{
pageAsset,
components: {},
isInsideEditor: isInsideEditor(),
}}
config={{ pathname: "/example" }}
/>
</main>
</div>
);
}
export default App;
Note: I’m using Tailwind CSS here to handle styling.
Right now, we are passing the component as an empty object. Let’s check out our /example page in dotCMS:
At this point, we have successfully connected our React app to dotCMS. Now, we can start adding content to the page by dragging items from the Content Palette and dropping them into the container — I’ll be adding a banner but you can add any contentType. Let’s see it:
After adding a contentlet to our page, we see a message saying “No component for the Banner contentType.” Remember the component's props we pass as an empty object early in this article? That’s the key.
import "./App.css";
import { useEffect, useState } from "react";
import { DotCmsClient } from "@dotcms/client";
import { DotcmsLayout } from "@dotcms/react";
const client = DotCmsClient.init({
dotcmsUrl: `${import.meta.env.VITE_DOTCMS_HOST_KEY}`,
authToken: `${import.meta.env.VITE_DOTCMS_AUTH_TOKEN_KEY}`,
siteId: `${import.meta.env.VITE_DOTCMS_SITE_ID_KEY}`,
});
function App() {
const [pageAsset, setPageAsset] = useState(null);
useEffect(() => {
client.page
.get({ path: "/example", language_id: 1 })
.then((pageAsset) => setPageAsset(pageAsset))
.catch((error) => console.log(error));
}, []);
if (!pageAsset) {
return <div>Loading...</div>;
}
return (
<div className="flex flex-col gap-6 min-h-screen bg-slate-200">
<main className="container m-auto">
<DotcmsLayout
pageContext={{
pageAsset,
components: {}, // Components Map as Empty Object
isInsideEditor: isInsideEditor(),
}}
config={{ pathname: "/example" }}
/>
</main>
</div>
);
}
export default App;
We’ll now need to build our custom banner component in our React app. Let’s create a new folder called content-types and define myBanner.jsx file there.
function Banner(contentlet) {
const { title, caption, image, link, buttonText } = contentlet;
const imagePath = `${import.meta.env.VITE_DOTCMS_HOST_KEY}/dA/${image}`;
return (
<div className="relative w-full p-4 bg-gray-200 h-96">
{image && (
<img
src={imagePath}
className="object-cover w-full h-full"
alt={title}
/>
)}
<div className="absolute inset-0 flex flex-col items-center justify-center p-4 text-center text-white">
<h2 className="mb-2 text-6xl font-bold text-shadow">
{contentlet.title}
</h2>
<p className="mb-4 text-xl text-shadow">{caption}</p>
<a
className="p-4 text-xl transition duration-300 bg-purple-500 rounded hover:bg-purple-600"
href={link || "#"}
>
{buttonText}
</a>
</div>
</div>
);
}
export default Banner;
Note: The prop received in our React component is the dotCMS contentlet defined by the contentType structure. Learn more about creating a content type here.
We are almost done! 🎉. Now we have to pass our Banner component to the DotcmsLayout component:
import "./App.css";
import { useEffect, useState } from "react";
import { DotCmsClient, isInsideEditor } from "@dotcms/client";
import { DotcmsLayout } from "@dotcms/react";
import Banner from "./content-types/Banner";
const client = DotCmsClient.init({
dotcmsUrl: `${import.meta.env.VITE_DOTCMS_HOST_KEY}`,
authToken: `${import.meta.env.VITE_DOTCMS_AUTH_TOKEN_KEY}`,
siteId: `${import.meta.env.VITE_DOTCMS_SITE_ID_KEY}`,
});
function App() {
const [pageAsset, setPageAsset] = useState(null);
useEffect(() => {
client.page
.get({ path: "/example", language_id: 1 })
.then((pageAsset) => setPageAsset(pageAsset))
.catch((error) => console.log(error));
}, []);
if (!pageAsset) {
return <div>Loading...</div>;
}
return (
<div className="flex flex-col gap-6 min-h-screen bg-slate-200">
<main className="container m-auto">
<DotcmsLayout
pageContext={{
pageAsset,
components: {
Banner: Banner,
},
isInsideEditor: isInsideEditor(),
}}
config={{ pathname: "/example" }}
/>
</main>
</div>
);
}
export default App;
Note: The components prop is a key/value map where the key is the contentType variable and the value is your React component. Learn more about contentType and contentlets here.
Let’s go to our dotCMS and see what we have done so far!
We did it 🥳! Our React app is now integrated with dotCMS. Let’s add some final touches.
Adding Header and Footer to the Page
The dotCMS PageAsset includes a Layout property with three boolean attributes: header, footer, and sidebar. These attributes can be either true or false, depending on the configuration in our dotCMS template. In our case, we want to create header and footer components and add them to the page if they are active in the template. That said, let’s create those components:
Header component
function Header() {
return (
<div className="flex items-center justify-between p-4 bg-purple-500">
<div className="flex items-center">
<h2 className="text-3xl font-bold text-white">
<a href="/">My React Page with dotCMS</a>
</h2>
</div>
</div>
);
}
export default Header;
Footer component
function Footer() {
return (
<footer className="p-4 text-white bg-purple-500 py-4">
<div className="flex w-full items-center justify-center">
<a href="https://www.dotcms.com/">
Check out our docs to learn more about dotCMS
</a>
</div>
</footer>
);
}
export default Footer;
Once we have them, let’s add them to the DotcmsLayout component in our main page:
import "./App.css";
import { useEffect, useState } from "react";
import { DotCmsClient, isInsideEditor } from "@dotcms/client";
import { DotcmsLayout } from "@dotcms/react";
import Header from "./layout/header";
import Footer from "./layout/Footer";
import Banner from "./content-types/Banner";
const client = DotCmsClient.init({
dotcmsUrl: `${import.meta.env.VITE_DOTCMS_HOST_KEY}`,
authToken: `${import.meta.env.VITE_DOTCMS_AUTH_TOKEN_KEY}`,
siteId: `${import.meta.env.VITE_DOTCMS_SITE_ID_KEY}`,
});
function App() {
const [pageAsset, setPageAsset] = useState(null);
useEffect(() => {
client.page
.get({ path: "/example", language_id: 1 })
.then((pageAsset) => setPageAsset(pageAsset))
.catch((error) => console.log(error));
}, []);
if (!pageAsset) {
return <div>Loading...</div>;
}
return (
<div className="flex flex-col gap-6 min-h-screen bg-slate-200">
{pageAsset?.layout.header && <Header />}
<main className="container m-auto">
<DotcmsLayout
pageContext={{
pageAsset,
components: {
Banner: Banner,
},
isInsideEditor: isInsideEditor(),
}}
config={{ pathname: "/example" }}
/>
</main>
{pageAsset?.layout.footer && <Footer />}
</div>
);
}
export default App;
After doing this, we’ll have a header and footer in our React app that’s editable with dotCMS 🤩.
Let’s make the page fully editable in dotCMS
We now have a React page within dotCMS. The next step is to make it fully editable. This will allow content authors to modify the layout or content, and see their changes right away on the page.
Let’s create a React hook called useDotCMS to connect the React app with the editor:
import { DotCmsClient, isInsideEditor } from "@dotcms/client";
import { useEffect, useState } from "react";
const client = DotCmsClient.init({
dotcmsUrl: `${import.meta.env.VITE_DOTCMS_HOST_KEY}`,
authToken: `${import.meta.env.VITE_DOTCMS_AUTH_TOKEN_KEY}`,
siteId: `${import.meta.env.VITE_DOTCMS_SITE_ID_KEY}`,
});
const useDotCMS = (pageParameter) => {
const [pageAsset, setPageAsset] = useState(null);
useEffect(() => {
client.page
.get(pageParameter)
.then((pageAsset) => setPageAsset(pageAsset))
.catch((error) => console.log(error));
}, [pageParameter]);
useEffect(() => {
// If we are not inside dotCMS, we won't listen to the edito
if (!isInsideEditor()) {
return;
}
// The editor will return the PageAsset everytime we do a change inside dotCMS
client.editor.on("changes", (page) => setPageAsset(page));
return;
}, []);
return { pageAsset };
};
export default useDotCMS;
This hook will be in charge of updating the page asset every time the editor updates it. To achieve this, we use the changes event of the Universal Visual Editor. That event emits every change in the page content. Additionally, we use the isInsideEditor function to check if we are in the UVE context.
After creating the hook, our Page Component should look like this:
import "./App.css";
import { isInsideEditor } from "@dotcms/client";
import { DotcmsLayout } from "@dotcms/react";
import useDotCMS from "./hooks/useDotCMS";
import Header from "./layout/header";
import Footer from "./layout/Footer";
import Banner from "./content-types/Banner";
function App() {
const { pageAsset } = useDotCMS({ path: "/example", language_id: 1 });
if (!pageAsset) {
return <div>Loading...</div>;
}
return (
<div className="flex flex-col gap-6 min-h-screen bg-slate-200">
{pageAsset?.layout.header && <Header />}
<main className="container m-auto">
<DotcmsLayout
pageContext={{
pageAsset,
components: {
Banner: Banner,
},
isInsideEditor: isInsideEditor(),
}}
config={{ pathname: "/example" }}
/>
</main>
{pageAsset?.layout.footer && <Footer />}
</div>
);
}
export default App;
And that’s it 🎉! We’ve successfully connected a React app to dotCMS and made it fully editable.
Let's see the final result of the tutorial:
Conclusion and Next Steps
In this tutorial, we’ve successfully connected a React app to dotCMS, fetched page assets, rendered dynamic content, and made it editable 🥳 🚀. For the next steps, consider exploring dotCMS Content Types or the Page API to suit your app's needs.