dot CMS

How to Integrate Your Astro App with dotCMS Using the JavaScript SDK

How to Integrate Your Astro App with dotCMS Using the JavaScript SDK
Author image

Rafael Velazco

Senior Software Engineer

Share this article on:

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 web developer, you’re familiar with building dynamic web applications. Now, imagine leveraging your 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 Astro 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 want 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:

  1. dotCMS Instance ✨: You can use the demo instance here.

  2. Node.js and npm: If you don’t have these installed, follow the Node.js Installation Guide.

  3. Astro App: If you need a starting point, check out the Astro Starter

  4. React: Astro supports React Components; check out the @astrojs/react docs

  5. Tailwind CSS: This tutorial uses Tailwind CSS for styling. Refer to the Tailwind Installation Guide.


What We’re Building

In this guide, we’ll build a simple Astro app that integrates with dotCMS, fetching and rendering dynamic page content. The app will support editing via the dotCMS dotCMS Universal Visual Editor (UVE).

80

Note: If you get stuck at any point in this tutorial, you can find the full code in this GitHub repository: rjvelazco/dotcms-astro-integration-example. You can also follow along by checking 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 organizes content with the following hierarchy:

  • RowsColumnsContainersContent (e.g., news, events, blog posts).

This structure simplifies content organization on dotCMS pages.

Example Diagram:

Image6.jpg

Learn more about the 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:

  1. Browse to Site > Pages.

  2. Click on Create Page and select Page Type.

  3. Name the page example and select the System Template.

  4. Publish the page.

80

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:

  1. Sign in to your dotCMS instance

  2. Browse to Settings -> Apps

  3. Select the built-in integration for UVE - Universal Visual Editor.

  4. Select the site that will be feeding the destination pages.

80

Then, add the following JSON configuration for development mode:

{
  "config": [
    {
      "pattern": ".*",
      "url": "http://localhost:4321/"
    }
  ]
}

The key pattern is a regular expression (RegEx) that identifies which URLs should be associated with the Astro application, while the URL key specifies the app’s location.

Note: if you want to know more about the Universal Visual Editor configuration and all you can do, check out our documentation: Universal Visual Editor Configuration for Headless Pages.


Building Our Astro + dotCMS Page

Project Folder Structure

Organize your project with the following structure:

🗂️ src
├── 🗂️ Components
├── 🗂️ pages
│ └── 📄 [...slug].astro
├── 🗂️ react
│ ├── 🗂️ Hook
│ ├── 🗂️ content-types
│ ├── 🗂️ Components
│ └── 📄 MyPage.tsx
└── 📄 client.ts

As you can see, we’ll be using React Components in our Astro App.

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. Let’s init our client in the client.ts file under the src folder:

import { DotCMSCmsClient } from "@dotcms/client";

export const client = DotCMSCmsClient.init({
  dotcmsUrl: import.meta.env.PUBLIC_DOTCMS_HOST,
  authToken: import.meta.env.PUBLIC_DOTCMS_AUTH_TOKEN,
  siteId: import.meta.env.PUBLIC_DOTCMS_SITE_ID
});

Replace the environment variables with your dotCMS instance details:

Let’s fetch the Page Asset

We are going to 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 searchParams.

For more details about the Page API parameters check out our documentation.

File: src/pages/[...slug].astro:

---
import "../styles/global.css";

// IMPORTS
import { client } from "../client";
import Layout from '../layouts/Layout.astro';
import MyPage from "../react/MyPage";

const { slug = "/" } = Astro.params;
const { searchParams } = Astro.url;
const pageAsset = await client.page.get({ path: slug, ...searchParams });
---
<Layout>
   <MyPage client:only="react" pageAsset={pageAsset} pathname={slug} />
</Layout>

We use a React component to help us view the response in the browser console. Let’s build that basic page component:

File: src/react/MyPage.tsx:

import { useEffect } from 'react';

const MyPage= ({ pageAsset pathname }) => {
    useEffect(() => {
        console.log("DOTCMS PAGE: ", pageAsset);
    }, [])
  return <div>EMPTY ASTRO PAGE</div>
};

export default MyPage;

Now, if we check the console we’ll be able to see the page response ⭐

Image2.png

Render de Our Page with @dotcms/react

Astro supports React components, so 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 { isInsideEditor } from "@dotcms/client";
import { DotcmsLayout } from "@dotcms/react";

import "../client";

const MyPage = ({ pageAsset, pathname }) => {
  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 }}
        />
      </main>
    </div>
  );
};

export default MyPage;

Right now, we are passing the component as an empty object, let’s check out our /example page in dotCMS:

Image1.png

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:

Image5.png

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 { isInsideEditor } from "@dotcms/client";
import { DotcmsLayout } from "@dotcms/react";

import "../client";

const MyPage = ({ pageAsset, pathname }) => {
  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 }}
        />
      </main>
    </div>
  );
};

export default MyPage;

We’ll now need to build our customs banner component in our Astro app. Let’s define our Banner component at this path: src/react/content-types/Banner.tsx

function Banner(contentlet) {
  const { title, caption, image, link, buttonText } = contentlet;
  const imagePath = `${import.meta.env.PUBLIC_DOTCMS_HOST}/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 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 it to the DotcmsLayout component:

import { isInsideEditor } from "@dotcms/client";
import { DotcmsLayout } from "@dotcms/react";

import Banner from "./content-types/Banner";

import "../client";

const MyPage = ({ pageAsset, pathname}) => {

  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 }}
        />
      </main>
    </div>
  );
};

export default MyPage;

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!

Image4.png

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

File: src/react/components/Header.tsx

function Header() {
  return (
    <div className="flex items-center justify-between p-4 bg-blue-500">
      <div className="flex items-center">
        <h2 className="text-3xl font-bold text-white">
          <a href="/">My Astro Page with dotCMS</a>
        </h2>
      </div>
    </div>
  );
}

export default Header;

Footer component

File: src/react/components/Footer.tsx

function Footer() {
  return (
    <footer className="p-4 text-white bg-blue-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:

import { isInsideEditor } from "@dotcms/client";
import { DotcmsLayout } from "@dotcms/react";

import Banner from "./content-types/Banner";
import Header from "./components/Header";
import Footer from "./components/Footer";

import "../client";

const MyPage = ({ pageAsset, pathname }) => {
  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 }}
        />
      </main>
      {pageAsset?.layout.footer && <Footer />}
    </div>
  );
};

export default MyPage;

Returning to our dotCMS instance, we’ll have an amazing header and footer for our Astro application 🤩.

Image3.png

Let’s make the page fully editable in dotCMS

We now have an Astro 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 make a React hook called useDotCMS to connect the Astro App with the Editor:

File: src/react/hook/useDotCMS.tsx

import { isInsideEditor } from "@dotcms/client";
import { useEffect, useState } from "react";
import { client } from "../../client";

const useDotCMS = (initialPageAsset) => {
  const [pageAsset, setPageAsset] = useState(initialPageAsset);

  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 – this 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 { isInsideEditor } from "@dotcms/client";
import { DotcmsLayout } from "@dotcms/react";

import Banner from "./content-types/Banner";
import Header from "./components/Header";
import Footer from "./components/Footer";
import useDotCMS from "./hook/useDotCMS";

import "../client";

const MyPage = ({ initialPageAsset, pathname}) => {

  const { pageAsset } = useDotCMS(initialPageAsset);

  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 }}
        />
      </main>
      {pageAsset?.layout.footer && <Footer />}
    </div>
  );
};

export default MyPage;

Note: Notice that we update the pageAsset prop to initialPageAsset. This is to avoid overwriting the variable, so we also need to update the [...slug].astro page:

---
import "../styles/global.css";

// IMPORTS
import { client } from "../client";
import Layout from '../layouts/Layout.astro';
import MyPage from "../react/MyPage";

const { slug = "/" } = Astro.params;
const { searchParams } = Astro.url;
const pageAsset = await client.page.get({ path: slug, ...searchParams });
---
<Layout>
   <MyPage client:only="react" initialPageAsset={pageAsset} pathname={slug} />
</Layout>

And that’s it 🎉! We’ve successfully connected an Astro app to dotCMS and made it fully editable.

Let's see the final result of the tutorial:

80

Conclusion and Next Steps

In this tutorial, we’ve successfully connected an Astro 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.