The dotCMS Universal Visual Editor (aka UVE) is a framework-agnostic editor that enables full in-context editing for headless websites, single-page applications (SPAs), and other remote web applications that use dotCMS as a content source.
If you're developing applications with Angular, you can integrate UVE to provide a seamless visual editing experience, leveraging the robust architectures of both Angular and dotCMS. By implementing it, you enable users to interact with the interface more dynamically, allowing for instant updates and better content management within your application.
In this guide, we will walk through the integration of UVE into an Angular project from scratch, ensuring a clean and efficient implementation. We'll cover everything from installation to creating a functional component that enables in-context visual editing of your content.
Note: The dotCMS Angular library is compatible and supported starting from Angular 17.
Prerequisites
Before integrating UVE into your Angular project, ensure that your development environment is properly set up. You'll need:
dotCMS instance: You can use the demo instance located here.
Node.js & npm: Follow the NodeJS Installation guide.
Angular CLI: Install it by following this Setting Angular Local environment guide.
An Angular application: You can create your project using this Creating your first Angular app official guide.[a][b]
Basic knowledge of Tailwind CSS: 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 Angular. 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: https://github.com/KevinDavilaDotCMS/dotcms-angular-integration-example. You can also follow along step by step by checking out the commit history here: Commit History.
dotCMS Basic knowledge
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
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:4200/"
}
]
}
Where:
pattern: a regular expression (RegEx) that identifies to dotCMS which URLs should be associated with the Angular application
url: the location of our Angular 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 Angular + dotCMS Page
To keep our project structured and maintainable, we will organize it as follows:
🗂️ src/
├── 🗂️ environments/
└── 🗂️ app/
├── 🗂️ content-types/
├── 🗂️ layout/
├── 🗂️ pages/
├── app.component.html
├── app.component.ts
└── app.component.scss
The first step in setting up our environment is ensuring we have the environment files.
If they are missing, you have two options:
Create them manually within the src directory.
Use the Angular CLI schematic, which will generate the environments folder along with two files:
environment.ts
environment.development.ts
ng generate environments
By running the provided command in your project's root directory, you'll have these files set up automatically.
These files will store environment variables related to dotCMS, ensuring a clean and centralized configuration. Next, we add this to the environments and fill it with our information
export const environment = {
dotcmsURL: 'YOU_DOTCMS_HOST',
authToken: 'YOUR_TOKEN',
siteId: 'YOUR_DOTCMS_SITE_ID'
};
We will also create a component inside the pages/ directory, which will be responsible for routing our application and rendering content based on the desired path.
To achieve this, run the following command:
ng generate component pages --inline-template=true --inline-style=true --skip-tests
This will generate a new file: pages.component.ts
Next, we need to set this component as our main route. To do this, we will navigate to app.routes and configure it accordingly:
/* app.routes.ts */
import { Routes } from '@angular/router';
import { PagesComponent } from './pages/pages.component';
export const routes: Routes = [
{
path: '',
component: PagesComponent
},
{
path: '**',
component: PagesComponent
}
];
And then modify the AppComponent
/* app.component.ts */
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
imports: [RouterOutlet],
styleUrl: './app.component.scss'
})
export class AppComponent { }
/* app.component.html */
<router-outlet />
With this setup, whenever we access any route in our application, the PagesComponent will be rendered.
Fetching a Page from dotCMS
The next step is to install the dotCMS client library SDK, part of our Javascript SDK. This library provides the necessary methods to interact seamlessly with dotCMS APIs, allowing us to retrieve and manage content efficiently.
npm i @dotcms/client
After installing the SDK library, we need to configure the client with our specific values. To do this, we must add the following configuration inside the app.config.ts file.
export const DOTCMS_CLIENT_TOKEN = new InjectionToken<DotCmsClient>('DOTCMS_CLIENT');
const DOTCMS_CLIENT_CONFIG: ClientConfig = {
dotcmsUrl: environment.dotcmsURL,
authToken: environment.authToken,
siteId: environment.siteId,
};
const client = DotCmsClient.init(DOTCMS_CLIENT_CONFIG);
export const appConfig: ApplicationConfig = {
providers: [
...existing configs,
{
provide: DOTCMS_CLIENT_TOKEN,
useValue: client
},
],
};
Where:
dotcmsURL: The URL of your dotCMS instance.
authToken: The JWT token of the dotCMS user you are using. If you don't have one, you can generate it by following these instructions: Creating an API Token in the UI.
siteId: The ID of the site from which you want to retrieve information. If left empty, the client will use the default site. Learn more here: Multi-Site Management in dotCMS.
Fetching the Page Asset
Now that our environment is fully set up, we are ready to retrieve content from our dotCMS instance!
We can use the client.page.get() method to fetch the content of any page within our dotCMS instance. This method accepts an object of type PageApiOptions as a parameter.
For this example, we will use the path provided in the URL along with the default language_id.
/* pages.component.ts */
import { Component, inject } from '@angular/core';
import { DOTCMS_CLIENT_TOKEN } from '../app.config';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-pages',
imports: [],
template: `
<p>
pages works!
</p>
`,
styles: ``
})
export class PagesComponent {
#dotcmsClient = inject(DOTCMS_CLIENT_TOKEN);
#route = inject(ActivatedRoute);
ngOnInit() {
this.#route.url.subscribe((urlSegments) => {
const pathname = urlSegments.join('/');
this.#fetchPageAsset(pathname);
});
}
#fetchPageAsset(pathname: string) {
this.#dotcmsClient.page.get({ path: pathname, language_id: 1 })
.then((page) => {
console.log("DotCMS Page Asset: ",page);
})
.catch((error) => {
console.error("Error fetching page asset: ",error);
});
}
}
If we check the console, we will see the response containing all the details of our Page Asset.
With just a few lines of code, we can retrieve all the necessary information from our dotCMS instance 🚀
Rendering Our Page with @dotcms/angular
To render our page, we will use the Angular SDK library provided by dotCMS. This library allows us to render a page using the Page Asset along with a component mapping system (which we will cover in more detail later).
Step 1: Install the Library
Before proceeding, we need to install the Angular library for dotCMS.
npm i @dotcms/angular
Now we can use the <dotcms-layout /> component to render our page. This component requires the following inputs:
pageAsset: The page object we receive from the client.page.get() function. It represents our page.
components: A key-value object where the key is the name of an element in our page, and the value is the component that will be rendered. This allows us to dynamically render different components based on each page element.
For now, we will pass an empty object to components.
/* pages.component.ts */
import { Component, inject, signal } from '@angular/core';
import { DotcmsLayoutComponent, DotCMSPageAsset, DotCMSPageComponent } from '@dotcms/angular';
import { DOTCMS_CLIENT_TOKEN } from '../app.config';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-pages',
imports: [DotcmsLayoutComponent],
template: `
@if (page(); as page) {
<div class="flex flex-col min-h-screen">
<main class="container m-auto">
<dotcms-layout
[pageAsset]="page"
[components]="components()"
/>
</main>
</div>
}
`,
styles: ``
})
export class PagesComponent {
#dotcmsClient = inject(DOTCMS_CLIENT_TOKEN);
#route = inject(ActivatedRoute);
page = signal<DotCMSPageAsset | null>(null);
components = signal<DotCMSPageComponent>({})
ngOnInit() {
this.#route.url.subscribe((urlSegments) => {
const pathname = urlSegments.join('/');
this.#fetchPageAsset(pathname);
});
}
#fetchPageAsset(pathname: string) {
this.#dotcmsClient.page.get({ path: pathname, language_id: 1 })
.then((page) => {
this.page.set(page as DotCMSPageAsset);
})
.catch((error) => {
console.error("Error fetching page asset: ",error);
});
}
}
With these changes, if we go to UVE and view our page, it will look like this:
This is expected because our page doesn't have any content yet, just an empty container. To add content, we will insert a contentlet of type "Banner." You can drag it from the Content Palette.
At this point, our page will display this message: "No component for Banner."
This happens because the <dotcms-layout /> component is trying to render a component for the Banner element we added. However, since we haven’t provided one yet, it falls back to a default message.
Now, let’s specify which component should be rendered for the Banner. To do this, we will create a new component inside the content-types folder.
We can generate this component using the Angular CLI.
ng generate component content-types/banner --inline-template=true --inline-style=true --skip-tests
And the content of this component will be:
/* banner.component.ts */
import { Component, input, OnInit } from '@angular/core';
import { DotCMSContentlet } from '@dotcms/angular';
@Component({
selector: 'app-banner',
imports: [],
template: `
<p>
banner works!
</p>
`,
styles: ``
})
export class BannerComponent implements OnInit {
contentlet = input.required<DotCMSContentlet>();
ngOnInit() {
console.log("Banner Contentlet: ",this.contentlet());
}
}
Now, we will go to pages.component.ts and add our new component to the components map.
/* pages.component.ts */
import { Component, inject, signal } from '@angular/core';
import { DotcmsLayoutComponent, DotCMSPageAsset } from '@dotcms/angular';
import { DOTCMS_CLIENT_TOKEN } from '../app.config';
import { DotCMSPageComponent } from '@dotcms/angular';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-pages',
imports: [DotcmsLayoutComponent],
template: `
@if (page(); as page) {
<div class="flex flex-col min-h-screen">
<main class="container m-auto">
<dotcms-layout
[pageAsset]="page"
[components]="components()"
/>
</main>
</div>
}
`,
styles: ``
})
export class PagesComponent {
#dotcmsClient = inject(DOTCMS_CLIENT_TOKEN);
#route = inject(ActivatedRoute);
page = signal<DotCMSPageAsset | null>(null);
components = signal<DotCMSPageComponent>({
Banner: import('../content-types/banner/banner.component').then(m => m.BannerComponent)
})
ngOnInit() {
this.#route.url.subscribe((urlSegments) => {
const pathname = urlSegments.join('/');
this.#fetchPageAsset(pathname);
});
}
#fetchPageAsset(pathname: string) {
this.#dotcmsClient.page.get({ path: pathname, language_id: 1 })
.then((page) => {
this.page.set(page as DotCMSPageAsset);
})
.catch((error) => {
console.error("Error fetching page asset: ",error);
});
}
}
If we check the console inside UVE, we will see something like this:
Internally, the <dotcms-layout /> component passes the Banner data as an input to our component. Because of this, our component needs an input of type dotCMSContentlet.
This input represents our Banner, and within our component, we can manipulate it however we like.
For this example, we are using Tailwind CSS to render a simple banner.
Additionally, we parse the image included in the Banner to use it on our website. Since the contentlet is a signal, we can take advantage of it by using a computed property.
/* banner.component.html */
import { Component, computed, input, OnInit } from '@angular/core';
import { DotCMSContentlet } from '@dotcms/angular';
import { environment } from '../../../environments/environment.development';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-banner',
imports: [RouterLink],
template: `
<div
class="flex overflow-hidden relative justify-center items-center w-full h-96 bg-gray-200"
>
@if (contentlet().image; as image) {
<img
class="object-cover w-full"
[src]="imageURL()"
[alt]="contentlet().title"
fill
priority
/>
}
<div
class="flex absolute inset-0 flex-col justify-center items-center p-4 text-center text-white"
>
<h2 class="mb-2 text-6xl font-bold text-shadow">
{{ contentlet().title }}
</h2>
<p class="mb-4 text-xl text-shadow">{{ contentlet()['caption'] }}</p>
<a
class="p-4 text-xl bg-red-400 rounded transition duration-300 hover:bg-red-500"
[routerLink]="contentlet()['link']"
>
{{ contentlet()['buttonText'] }}
</a>
</div>
</div>
`,
styles: ``
})
export class BannerComponent {
contentlet = input.required<DotCMSContentlet>();
imageURL = computed(() => {
return environment.dotcmsURL + '/dA/' + this.contentlet().image;
});
}
And our page should look like this:
Our Angular application now renders our dotCMS page, Great 👏!
Let's move on to the final details.
Adding Header & Footer
The dotCMS PageAsset includes a Layout property with two boolean attributes: header and footer.
/* PageAsset*/
{
canCreateTemplate: booleanM
containers: {},
layout: {
...
footer: boolean,
header: boolean,
...
},
}
These attributes can be either true or false, depending on the configuration of 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 go ahead and create those components:
ng generate component layout/header --inline-template=true --inline-style=true --skip-tests
ng generate component layout/footer --inline-template=true --inline-style=true --skip-tests
Header
import { Component } from '@angular/core';
@Component({
selector: 'app-header',
template: `
<div class="flex items-center justify-between p-4 bg-red-400">
<div class="flex items-center">
<h2 class="text-3xl font-bold text-white">
<a href="/">My Angular Page with dotCMS</a>
</h2>
`,
styles: ``
})
export class HeaderComponent {}
Footer
import { Component } from '@angular/core';
@Component({
selector: 'app-footer',
template: `
<footer class="p-4 text-white bg-red-400 py-4">
<div class="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>
`,
styles: ``
})
export class FooterComponent { }
And now we can use both in our main component:
/* pages.component.ts */
import { Component, inject, signal } from '@angular/core';
import { DotcmsLayoutComponent, DotCMSPageAsset } from '@dotcms/angular';
import { DOTCMS_CLIENT_TOKEN } from '../app.config';
import { DotCMSPageComponent } from '@dotcms/angular';
import { ActivatedRoute } from '@angular/router';
import { HeaderComponent } from "../layout/header/header.component";
import { FooterComponent } from "../layout/footer/footer.component";
@Component({
selector: 'app-pages',
imports: [DotcmsLayoutComponent, HeaderComponent, FooterComponent],
template: `
@if (page(); as page) {
<div class="flex flex-col min-h-screen">
@if(page.layout.header) {
<app-header />
}
<main class="container m-auto">
<dotcms-layout
[pageAsset]="page"
[components]="components()"
/>
</main>
@if(page.layout.footer) {
<app-footer />
}
</div>
}
`,
styles: ``
})
export class PagesComponent {
#dotcmsClient = inject(DOTCMS_CLIENT_TOKEN);
#route = inject(ActivatedRoute);
page = signal<DotCMSPageAsset | null>(null);
components = signal<DotCMSPageComponent>({
Banner: import('../content-types/banner/banner.component').then(m => m.BannerComponent)
})
ngOnInit() {
this.#route.url.subscribe((urlSegments) => {
const pathname = urlSegments.join('/');
this.#fetchPageAsset(pathname);
});
}
#fetchPageAsset(pathname: string) {
this.#dotcmsClient.page.get({ path: pathname, language_id: 1 })
.then((page) => {
console.log("Page Asset: ",page);
this.page.set(page as DotCMSPageAsset);
})
.catch((error) => {
console.error("Error fetching page asset: ",error);
});
}
}
And now our page should look like this:
We now have our page rendering the Header, Footer, and our contentlets. Let's add a few final touches. 😎
Let’s make the page fully editable in dotCMS
At this point, we can render our page in our Angular application. The next step is to make it fully editable.
We will add an extra function to our app component.
/* pages.component.ts */
import { Component, inject, signal } from '@angular/core';
import { DotcmsLayoutComponent, DotCMSPageAsset, DotCMSPageComponent } from '@dotcms/angular';
import { DOTCMS_CLIENT_TOKEN } from '../app.config';
import { ActivatedRoute } from '@angular/router';
import { HeaderComponent } from "../layout/header/header.component";
import { FooterComponent } from "../layout/footer/footer.component";
import { isInsideEditor } from '@dotcms/client';
@Component({
selector: 'app-pages',
imports: [DotcmsLayoutComponent, HeaderComponent, FooterComponent],
template: `
@if (page(); as page) {
<div class="flex flex-col min-h-screen">
@if(page.layout.header) {
<app-header />
}
<main class="container m-auto">
<dotcms-layout
[pageAsset]="page"
[components]="components()"
/>
</main>
@if(page.layout.footer) {
<app-footer />
}
</div>
}
`,
styles: ``
})
export class PagesComponent {
#dotcmsClient = inject(DOTCMS_CLIENT_TOKEN);
#route = inject(ActivatedRoute);
page = signal<DotCMSPageAsset | null>(null);
components = signal<DotCMSPageComponent>({
Banner: import('../content-types/banner/banner.component').then(m => m.BannerComponent)
})
ngOnInit() {
if (isInsideEditor()) {
this.#listenToEditorChanges();
}
this.#route.url.subscribe((urlSegments) => {
const pathname = urlSegments.join('/');
this.#fetchPageAsset(pathname);
});
}
#fetchPageAsset(pathname: string) {
this.#dotcmsClient.page.get({ path: pathname, language_id: 1 })
.then((page) => {
console.log("Page Asset: ",page);
this.page.set(page as DotCMSPageAsset);
})
.catch((error) => {
console.error("Error fetching page asset: ",error);
});
}
#listenToEditorChanges() {
this.#dotcmsClient.editor.on('changes', (page) => {
if (!page) {
return;
}
this.page.set(page as DotCMSPageAsset);
});
}
}
We’ve added the listenToEditorChanges function, which will handle updating the page asset every time the editor makes changes. To achieve this, we use the changes event from the Universal Visual Editor. This event emits every change made to the page content. Additionally, we use the isInsideEditor function to check if we’re in the UVE context.
And that’s it 🎉! We’ve connected our Angular application with dotCMS and made it fully editable.
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.