This project demonstrates how to build dynamic, fully editable pages using dotCMS as a headless CMS with a Next.js front end. By combining these technologies, you can:
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ │ │ │ │ │
│ dotCMS │──────▶ Next.js │──────▶ Browser │
│ (Content) │ │ (Front end) │ │ (Viewing) │
│ │ │ │ │ │
└───────────────┘ └───────────────┘ └───────────────┘
▲ │ │
│ │ │
└──────────────────────┴──────────────────────┘
Universal Visual Editor (UVE)
(Content Editing)The integration uses dotCMS APIs to fetch content and the Universal Visual Editor to enable in-context editing directly on your Next.js pages.
See a live example at https://nextjs-example-sigma-five.vercel.app/.
The example above is a Next.js front end for the dotCMS demo site, and changes to pages and content on the latter will be reflected, there. For more information on the demo site, see the relevant section below.
Before you begin, make sure you have:
These packages are already included in the example project's dependencies, so you don't need to install them separately.
This example uses the following npm packages from dotCMS:
| Package | Purpose | Description |
|---|---|---|
| @dotcms/client | API Communication | Core API client for fetching content from dotCMS |
| @dotcms/react | UI Components | React components and hooks for rendering dotCMS content |
| @dotcms/uve | Visual Editing | Universal Visual Editor integration |
| @dotcms/types | Type Safety | TypeScript type definitions for dotCMS |
| @dotcms/experiments | A/B Testing | A/B testing capabilities (included; not used in Page.tsx rendering) |
This guide will walk you through the process of setting up the dotCMS Next.js example from scratch.
Use one of the following commands to create a new Next.js app with the dotCMS example:
# Using npm
npx create-next-app dotcms-nextjs-demo --example https://github.com/dotCMS/core/tree/main/examples/nextjs
# Using Yarn
yarn create next-app dotcms-nextjs-demo --example https://github.com/dotCMS/core/tree/main/examples/nextjs
# Using pnpm
pnpm create next-app dotcms-nextjs-demo --example https://github.com/dotCMS/core/tree/main/examples/nextjsThis will create a new directory with the example code and install all necessary dependencies.
First, get a dotCMS Site. If you want to test this example, you can also use our demo site.
If using the demo site, you can log in with these credentials:
| User Name | Password |
|---|---|
| admin@dotcms.com | admin |
Once you have a site, you can log in with the credentials and start creating content.
Make your API Token had read-only permissions for Pages, Folders, Assets, and Content. Using a key with minimal permissions follows security best practices.
This integration requires an API Key with read-only permissions for security best practices:
For detailed instructions, please refer to the dotCMS API Documentation - Read-only token.
The Universal Visual Editor (UVE) is a critical feature that creates a bridge between your dotCMS instance and your Next.js application. This integration Enables real-time visual editing and allows content editors to see and modify your actual Next.js pages directly from within dotCMS.
To set up the Universal Visual Editor:
{
"config": [
{
"pattern": "(.*)",
"url": "http://localhost:3000"
}
]
}For detailed instructions, see the dotCMS UVE Headless Configuration.
This configuration tells dotCMS that when editors are working on content in the admin panel, they should see your Next.js application running at http://localhost:3000. The pattern (.*) means this applies to all pages in your site.
Create a .env.local file in the root of the project by running the following command:
# This will create a new file with the correct variables.
cp .env.local.example .env.localThen set each variable in the .env.local file:
NEXT_PUBLIC_DOTCMS_HOST: The URL of your dotCMS site.NEXT_PUBLIC_DOTCMS_AUTH_TOKEN: The API Key you created in Step 2B.NEXT_PUBLIC_DOTCMS_SITE_ID: The site key of the site you want to use.The site ID variable refers to the site that will be used to pull content into your Next.js app. dotCMS is a multi-site CMS, meaning a single instance can manage multiple websites; the site ID specifies which site's content should be pulled into your Next.js app. If left empty or given an incorrect value, content will be pulled from the default site configured in dotCMS.
You can find the values for this variable — site keys or identifiers both work, though keys are simpler and more recommended — under System > Sites. Learn more about dotCMS Multi-Site management here.
Run the development server with one of the following commands:
# Using npm
npm run dev
# Using Yarn
yarn dev
# Using pnpm
pnpm devYou should see a message in your terminal indicating that the Next.js app is running at http://localhost:3000. Open this URL in your browser to see your dotCMS-powered Next.js site.
After setting up the Universal Visual Editor and running your Next.js application, you can edit your page in the Universal Visual Editor:
Learn more about the Universal Visual Editor here.
The integration between dotCMS and Next.js works by:
src/
├── app/ # App Router pages (Server-Side Rendered, .tsx)
│ ├── [[...slug]]/ # Dynamic catch-all routing
│ │ └── page.tsx # Rendering rules for the specified route
│ ├── blog/ # Blog pages
│ │ ├── post/[[...slug]] # Further dynamic routing
│ │ │ └── page.tsx # Rendering rules for the specified route
│ │ └── page.tsx # Rendering for `blog/` root page
│ ├── layout.tsx # Root layout
│ ├── not-found.tsx # 404 page
│ └── globals.css # Global styles
├── components/
│ ├── content-types/ # One component per dotCMS Content Type
│ │ ├── index.ts # Content Type → React component mapping (pageComponents)
│ │ └── *.tsx # Individual Content Type components
│ ├── editor/ # UVE editor buttons (EditButton, ReorderMenuButton)
│ ├── header/ footer/ forms/ # Site chrome
│ └── *.tsx # error, ErrorLayout, BlogCard, DestinationListing, ...
├── config/
│ └── dotcms.config.ts # Typed, centralized environment access
├── hooks/ # Custom React hooks (useIsEditMode, useDebounce)
├── lib/
│ └── dotCMSClient.ts # dotCMS API client initialization
├── types/
│ └── content.ts # Shared TypeScript interfaces (Blog, Destination, ...)
├── utils/ # Utility functions
│ ├── getDotCMSPage.ts # Cached page fetch (page + GraphQL content)
│ ├── pageResponse.ts # Typed guards (isPageError, getPageContent, ...)
│ ├── queries.ts # GraphQL query strings
│ └── imageLoader.ts # Custom Next.js image loader
└── views/ # Client-side page templates (can use React hooks)
├── Page.tsx
├── DetailPage.tsx
└── BlogListingPage.tsxThis project is written in TypeScript (strict mode, with the @/* path alias configured in tsconfig.json) and uses Next.js with App Router for server-side rendering, with some important architectural decisions:
App Router (src/app/): Contains all server-side rendered pages and routes. These components don't use React hooks directly due to Next.js 13+ restrictions. Learn more about the App Router here.
Components (src/components/):
content-types/ folder contains React components that render dotCMS content.MyCustomContent content type in dotCMS, you would create a matching component in this folder to render it.Views (src/views/): Contains client-side page templates (Page.tsx, DetailPage.tsx, BlogListingPage.tsx) that can use React hooks. Since Next.js App Router components can't directly use hooks, these components handle client-side logic.
Config (src/config/): The dotcms.config.ts module provides typed, centralized access to the dotCMS environment variables, so every other module reads configuration from one place instead of touching process.env directly.
Lib (src/lib/): Contains the dotCMSClient.ts, which initializes the connection to your dotCMS instance.
Types (src/types/): Shared TypeScript interfaces (Blog, Destination, NavItem, ContentTypeProps, and more) used across the app for type-safe content rendering.
Content in this integration is fetched using the @dotcms/client package, which provides a streamlined way to communicate with the dotCMS API. This client handles authentication, request formatting, and response parsing automatically.
The process works as follows:
src/lib/dotCMSClient.tssrc/config/dotcms.config.ts, which centralizes typed access to the environment variablesHere's how the client is configured:
import { createDotCMSClient } from "@dotcms/client";
import {
dotCMSAuthToken,
dotCMSHost,
dotCMSSiteId,
} from "@/config/dotcms.config";
export const dotCMSClient = createDotCMSClient({
dotcmsUrl: dotCMSHost,
authToken: dotCMSAuthToken,
siteId: dotCMSSiteId,
logLevel: process.env.NODE_ENV === "development" ? "verbose" : "default",
requestOptions: {
// UVE needs fresh data so in-context edits are reflected immediately.
cache: "no-cache",
},
});And here's a typical page fetching function. It is wrapped in React's cache() so multiple callers within a single request (e.g. generateMetadata and the page body) share one network round-trip, and it requests extra GraphQL content alongside the page. On failure it returns { error } so callers can branch without try/catch — use the guards in @/utils/pageResponse to narrow the result:
import { cache } from "react";
import { dotCMSClient } from "@/lib/dotCMSClient";
import type { PageExtraContent } from "@/types/content";
import { blogQuery, destinationQuery, navigationQuery } from "@/utils/queries";
export const getDotCMSPage = cache(async (path: string) => {
try {
return await dotCMSClient.page.get<{ content: PageExtraContent }>(path, {
graphql: {
content: {
blogs: blogQuery,
destinations: destinationQuery,
navigation: navigationQuery,
},
},
});
} catch (error) {
return { error };
}
});Learn more about the @dotcms/client package here.
dotCMS allows a single page to be accessed via multiple URL paths (e.g., / and /index for the same "Home" page). This flexibility means your Next.js application needs to handle these variations.
To ensure all paths to the same content are properly managed and to prevent 404/500 errors, we recommend using a catch-all route strategy in Next.js.
How to Implement in Next.js:
Implement a dynamic route like [[...slug]] in your Next.js app. This route will capture all URL segments, allowing your application to correctly process any path dotCMS uses for your content.
You can learn more about Next.js routing strategies here
The rendering process for dotCMS content in Next.js involves several key components working together:
When a page is rendered:
useEditableDotCMSPage hook prepares it for potential editingDotCMSLayoutBody component renders the page structureHere's how this looks in code:
"use client";
import { DotCMSLayoutBody, useEditableDotCMSPage } from "@dotcms/react";
// Define custom components for specific Content Types
// The key is the Content Type variable name in dotCMS
const pageComponents = {
dotCMSProductContent: MyCustomDotCMSProductComponent,
dotCMSBlogPost: BlogPostComponent,
};
interface MyPageProps {
page: Parameters<typeof useEditableDotCMSPage>[0];
}
export function MyPage({ page }: MyPageProps) {
const { pageAsset, content } = useEditableDotCMSPage(page);
return (
<div>
<DotCMSLayoutBody page={pageAsset} components={pageComponents} />
</div>
);
}useEditableDotCMSPage hook will not modify the page object outside the editorDotCMSLayoutBody component renders both the page structure and contentpageComponents will be used to render Content TypesLearn more about the @dotcms/react package here.
One of the key concepts in this integration is mapping dotCMS Content Types to React components. This mapping tells the framework which React component should render which type of content from dotCMS.
How the mapping works:
// Example of mapping dotCMS Content Types to React components
const pageComponents = {
// The key "DotCMSProduct" must match a Content Type variable name in dotCMS
DotCMSProduct: ProductComponent,
// The key "DotCMSBlogPost" must match a Content Type variable name in dotCMS
DotCMSBlogPost: BlogPostComponent,
};What happens at runtime:
ProductComponent is renderedProductComponentExample of a component receiving contentlet data:
// The props passed to this component are the contentlet data from dotCMS.
// Declare an interface so each field is typed.
interface ProductProps {
title?: string;
price?: number;
description?: string;
image?: { url: string };
}
function ProductComponent({ title, price, description, image }: ProductProps) {
// Access fields defined in the DotCMSProduct Content Type
return (
<div className="product">
<h2>{title}</h2>
{image && <img src={image.url} alt={title} />}
<p className="price">${price}</p>
<p>{description}</p>
</div>
);
}This pattern allows you to create custom rendering for each type of content in your dotCMS instance, while maintaining a clean separation between content and presentation.
This mapping should be passed to the DotCMSLayoutBody component as shown in the previous section.
Learn more about dotCMS Content and Components:
This example focuses on content types, UVE, and general dotCMS + Next.js patterns. It does not integrate @dotcms/experiments in Page.tsx by design.
For a minimal, copy-paste reference of Content Analytics and A/B Experiments in Next.js App Router (with the safe withExperiments pattern), see:
examples/nextjs-analytics-experiments — canonical headless analytics + experiments exampleThis example demonstrates the powerful integration between dotCMS and Next.js, enabling fully editable and dynamic web pages. By leveraging dotCMS as a headless CMS and Next.js for front-end rendering, you can create high-performance websites that offer both developer flexibility and content editor ease-of-use.
Key benefits of this approach include:
To deepen your understanding of this integration, explore these official dotCMS resources:
Additional resources:
Found an issue with this documentation? View the source