Step by Step Guide to developing with nextjs front end and umbraco CMS.
Code Repository: https://github.com/AbdulmueezEmiola/umbraco-nextjs
In this article, we focus on integrating next js frontend with a headless umbraco cms. This is ideal for blogs, dynamic websites and websites controlled by non technical users due to the following reasons.
- Umbraco can be used to structure the dynamic content like blogs, event listing and marketing pages while next js handles the rendering of these pages with optimized performance.
- Due to the user friendly interface of umbraco, editors can easily focus on content and images of the platform.
- Next js accessibility features makes the website accessible to all users while umbraco’s built in support for multi language content makes it easy to expand websites globally.
Set up your development Environment
Before starting, ensure you have the following installed:
- Node.js: Download and install Node.js by following the instructions on the official Node.js website.
- .NET SDK: Install the .NET SDK by referring to the instructions provided on Download and Install .NET Core SDK.
- A Supported Database: Choose from MySQL, SQL Server, SQLite, or PostgreSQL. For simplicity, this guide uses SQLite as it requires minimal setup.
- Install umbraco templates using the following command
dotnet new install Umbraco.Templates
Once you have the prerequisites installed, the next step is to create a folder that will serve as the root for your project. For this guide, we’ll name it umbraco-nextjs
, but you can choose any name that suits your project. To create the folder, follow these steps:
- Open your terminal.
- Navigate to the location where you want the project folder to be created.
- Create the folder with the following command:
mkdir umbraco-nextjs
cd umbraco-nextjs
Create Umbraco CMS Backend
To create a new umbraco backend, we need to run the follow the following steps:
dotnet new umbraco --name umbracoApp
cd umbracoApp
dotnet run
The name umbracoApp can be changed to a more suitable option.
In the terminal, you will see the url where the umbraco application is running e.g https://localhost:44372. Navigate to the url and you will be shown a setup wizard for configuring umbraco. Follow the setup wizard to configure the CMS and create an admin user.
Next, we’ll set up a custom document type and corresponding content to test the integration. Follow these steps:
- Navigate to the Settings tab in the Umbraco backoffice.
- Create a new Document Type and name it Page.
- Add the following properties to the Page document type: Title & Description
- Navigate to the structure sub tab of the document type and enable
Allow at root.
- Once the document type is configured, switch to the Content tab.
- Create a new content item using the Page document type and populate it with sample data for testing.
Next, we need to set up the Content Delivery API in your Umbraco project. To do this, follow these steps:
- Open the
Program.cs
file in your project. - Locate the
builder.CreateUmbracoBuilder()
call. - Update it to include
.AddDeliveryApi()
so it matches the following code:
builder.CreateUmbracoBuilder()
.AddBackOffice()
.AddWebsite()
.AddComposers()
.AddDeliveryApi()
.Build();
Next, configure the Delivery API settings in the appsettings.json
file by following the steps below:
- Open the
appsettings.json
file in your project directory. - Locate the
Umbraco
section and itsCMS
subsection. - Add the following configuration under the
CMS
section:
"DeliveryApi": {
"Enabled": true,
"PublicAccess": false,
"ApiKey": {{Api Key}},
"RichTextOutputAsJson": false
}
- The
PublicAccess
property determines whether an API key is required to access the Delivery API. - Replace
{{Api Key}}
with a valid API key. You can generate one using tools like the API Key Generator.
Next, configure CORS (Cross-Origin Resource Sharing) on your server to allow requests from your frontend. Follow these steps:
- Open the
Program.cs
file in your project. - Add the following code before the line
WebApplication app = builder.Build();
to define a CORS policy:
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowLocalhost3000",
builder => builder.WithOrigins("http://localhost:3000") // Replace with your frontend URL
.AllowAnyMethod()
.AllowAnyHeader());
});
- After the line
WebApplication app = builder.Build();
, apply the CORS policy by adding:
app.UseCors("AllowLocalhost3000");
NB: Replace
"http://localhost:3000"
with the URL of your frontend application, if different.
Next, we need to configure the redirect url of the umbraco application by following the steps below:
- Open the
appsettings.json
file in your project directory. - Locate the
Umbraco
section and itsCMS
subsection. - Add the following configuration under the
CMS
section:
"WebRouting": {
"UmbracoApplicationUrl": "https://localhost:44372/"
}
Next, restart the Umbraco application to apply the changes and test the Delivery API setup. Once the application is running, open your browser and navigate to the Delivery API Swagger URL. The URL follows this format:
https://localhost:{port}/umbraco/swagger/index.html?urls.primaryName=Umbraco+Delivery+API
In there, we can view the available endpoints for the content delivery api.
Create NextJS Frontend
To create the next js app, we need to follow the following steps:
- Navigate to the root folder i.e
umbraco-nextjs
- Set up a nextjs app with the following code
npx create-next-app@latest nextjs-app
cd nextjs-app
The name nextjs-app can be renamed to a more suitable option. In this appliation, we are using typescript.
- In the configuration wizard, follow the steps outlined and choose the appropriate options.
- Define the following properties in the .env file, use the http port when developing locally.
NEXT_PUBLIC_UMBRACO_URL=http://localhost:{port}
NEXT_PUBLIC_UMBRACO_API_KEY={{API_KEY}}
NEXT_PUBLIC_UMBRACO_CONTENT_LANGUAGE=en-US
- Create a new folder named services in the src folder.
- Create a new file called service.umbraco.content.ts which will house all our helper functions for interacting with the umbraco delivery api. This code was gotten from here.
/**
* Tests that something is a valid guid
* @param input
* @returns
*/
function isValidGuid(input: string): boolean {
if (
!/^(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}$/.test(
input
)
) {
return false;
}
return true;
}
const cacheStrategy = "no-cache"; // 'force-cache' or 'no-store' https://nextjs.org/docs/app/api-reference/functions/fetch#optionscache
const UMBRACO_URL = process.env.NEXT_PUBLIC_UMBRACO_URL;
const UMBRACO_API_KEY = process.env.NEXT_PUBLIC_UMBRACO_API_KEY;
const UMBRACO_CONTENT_LANGUAGE =
process.env.NEXT_PUBLIC_UMBRACO_CONTENT_LANGUAGE;
/**
* Gets all site content in pages
*
* @param take The number of items to select from the content tree. Defaults to 10
* @param skip The number of items to skip from the content tree. Defaults to 0
* @param previewMode Set to `true` to see the pages in preview mode. Defaults to false
* @returns A collection of content items
*/
const getAllContentPagedAsync = async (
take: number = 10,
skip: number = 0,
previewMode: boolean = false
) => {
console.log(UMBRACO_URL);
const data = await fetch(
`${UMBRACO_URL}/umbraco/delivery/api/v2/content?skip=${skip}&take=${take}&fields=properties[contentBlocks,metaTitle,metaKeywords,metaDescription,relatedBlogPosts]`,
{
cache: cacheStrategy,
method: "GET",
headers: {
"Api-Key": `${UMBRACO_API_KEY}`,
"Accept-Language": `${UMBRACO_CONTENT_LANGUAGE}`,
Preview: `${previewMode}`,
},
}
);
const siteContent = await data.json();
return siteContent;
};
/**
* Gets a single page by its pagepath
* @param pagePath the page path (for example "/home")
* @param previewMode set to `true` to view the content in preview mode. Defaults to `false`
* @returns A single content item
*/
const getPageAsync = async (pagePath: string, previewMode: boolean = false) => {
if (pagePath == "/" || pagePath == "") {
pagePath = "/";
}
try {
const url: string = `${UMBRACO_URL}/umbraco/delivery/api/v2/content/item${pagePath}/?fields=properties%5B%24all%5D`;
const data = await fetch(`${url}`, {
cache: cacheStrategy,
method: "GET",
headers: {
"Accept-Language": `${UMBRACO_CONTENT_LANGUAGE}`,
Preview: `${previewMode}`,
"Content-Type": "application/json",
"Api-Key": `${UMBRACO_API_KEY}`,
},
});
if (!data.ok) {
throw new Error(`Failed to fetch data: ${data.statusText}`);
}
const pageContent = await data.json();
return pageContent;
} catch (error) {
console.error("Error fetching page content:", error);
return null;
}
};
/**
* Gets the ancestors of a document by the document's Umbraco ID
* @param documentId the Umbraco ID (Guid) of the queried document
* @param skip Used for paging, configures the number of entities to skip over
* @param take Used for paging, configures the max number of entities to return
* @returns a collection of Umbraco documents, each of which is the ancestor of the Content item
* @throws Error when the documentId is not a valid Guid
*/
const getAncestorsOfDocument = async (
documentId: string,
skip: number = 0,
take: number = 10,
previewMode: boolean = false
) => {
return getChildrenAncestorsOrDescendants(
documentId,
"ancestors",
skip,
take,
previewMode
);
};
/**
* Gets the Descendants of a document by the document's Umbraco ID
* @param documentId the Umbraco ID (Guid) of the queried document
* @param skip Used for paging, configures the number of entities to skip over
* @param take Used for paging, configures the max number of entities to return
* @returns a collection of Umbraco documents, each of which is the descendant of the Content item
* @throws Error when the documentId is not a valid Guid
*/
const getDescendantsOfDocument = async (
documentId: string,
skip: number = 0,
take: number = 10,
previewMode: boolean = false
) => {
return getChildrenAncestorsOrDescendants(
documentId,
"descendants",
skip,
take,
previewMode
);
};
/**
* Gets the Children of a document by the document's Umbraco ID
* @param documentId the Umbraco ID (Guid) of the queried document
* @param skip Used for paging, configures the number of entities to skip over
* @param take Used for paging, configures the max number of entities to return
* @returns a collection of Umbraco documents, each of which is the child of the Content item
* @throws Error when the documentId is not a valid Guid
*/
const getChildrenOfDocument = async (
documentId: string,
skip: number = 0,
take: number = 10,
previewMode: boolean = false
) => {
return getChildrenAncestorsOrDescendants(
documentId,
"children",
skip,
take,
previewMode
);
};
const getChildrenAncestorsOrDescendants = async (
documentId: string,
childrenAncestorOrDescendantsSpecifier: string = "children",
skip: number = 0,
take: number = 10,
previewMode: boolean = false
) => {
if (
childrenAncestorOrDescendantsSpecifier != "ancestors" &&
childrenAncestorOrDescendantsSpecifier != "descendants" &&
childrenAncestorOrDescendantsSpecifier != "children"
) {
throw Error(
`param 'childrenAncestorOrDescendantsSpecifier' must be either ancestor or descendant. Received ${childrenAncestorOrDescendantsSpecifier}`
);
}
if (!isValidGuid(documentId)) {
throw Error(
`param documentId must be a valid guid, received '${documentId}'`
);
}
const url = `${UMBRACO_URL}/umbraco/delivery/api/v2/content/?fields=properties[contentBlocks,metaTitle,metaKeywords,metaDescription]&fetch=${childrenAncestorOrDescendantsSpecifier}:${documentId}&skip=${skip}&take=${take}`;
// console.log('making request to ' + url)
const data = await fetch(`${url}`, {
cache: cacheStrategy,
method: "GET",
headers: {
"Api-Key": `${UMBRACO_API_KEY}`,
"Accept-Language": `${UMBRACO_CONTENT_LANGUAGE}`,
Preview: `${previewMode}`,
},
});
const umbracoDocuments = await data.json();
return umbracoDocuments;
};
export {
getAllContentPagedAsync as GetAllContentPagedAsync,
getPageAsync as GetPageAsync,
getAncestorsOfDocument as GetAncestorsOfDocumentAsync,
getDescendantsOfDocument as GetDescendantsOfDocumentAsync,
getChildrenOfDocument as GetChildrenOfDocumentAsync,
};
Next, we define the data structures for the application
- Inside the
src
directory, create a new folder namedmodels
. - Create a file named
base.ts
in themodels
folder and add the following code to define the base model interface:
export interface BaseModel<T> {
contentType: string;
createDate: string;
updateDate: string;
id: string;
name: string;
properties: T;
}
- Create a file named
listModel.ts
in themodels
folder and add the following code to define the interface for listing document types:
export interface ListModel<T> {
total: number;
items: T[];
}
- Create a file named
Page.ts
in themodels
folder and define the interface for the Page document type and list model as follows:
import { BaseModel } from "./base";
import { ListModel } from "./listModel";
export type PageModel = BaseModel<{
name: string;
description: string;
}>;
export type PageList = ListModel<PageModel>;
- In the
services
folder, create a file namedpage.ts
and add the following code to define a function for fetching Page data from Umbraco:
import { PageList, PageModel } from "@/models/page";
import {
GetAllContentPagedAsync,
GetPageAsync,
} from "./service.umbraco.content";
export const getAllPages = async () => {
const data = await GetAllContentPagedAsync();
return data as PageList;
};
export const getPage = async (id: string) => {
const data = await GetPageAsync(`/${id}`);
return data as PageModel;
};
};
- Navigate to the
src/app
directory, open thepage.tsx
file and replace its content with the following code to fetch and display the list of pages. This page will be used in navigating to the detailed page for each of the item.
import styles from "./page.module.css";
import { getAllPages } from "@/services/page";
export default async function Home() {
const pages = await getAllPages();
return (
<div className={styles.page}>
<main className={styles.main}>
<h1>Pages Available</h1>
<ul>
{pages.items.map((page) => (
<li key={page.id}>
Visit{" "}
<a href={`/${page.id}`} className={styles.primary}>
{page.name}
</a>
</li>
))}
</ul>
</main>
</div>
);
}
- In the
src/app
directory, create a new folder named[id]
and create a file called page.tsx within the[id]
folder. - Add the following code to fetch and display the details of a specific page:
import { getPage } from "@/services/page";
import styles from "../page.module.css";
export default async function Page({
params,
}: {
params: Promise<{ id: string }>;
}) {
const id = (await params).id;
const data = await getPage(id);
return (
<div className={styles.page}>
<main className={styles.main}>
<h1>{data.name}</h1>
<div>
<span>Title: </span>
<span>{data.properties.title}</span>
</div>
<div>
<span>Description: </span>
<span>{data.properties.description}</span>
</div>
</main>
</div>
);
}
Afterwards run the application using the code npm run dev.
The nextjs application should be accessible via http://localhost:3000
.