Discover the full potential of Umbraco 14/15’s new backoffice! This guide walks you through every step of extending the frontend-focused architecture. Learn to set up custom dashboards, create intuitive menus, use Context API, and build dynamic routes all through hands on examples.
Intro
As you know, the new backoffice is finally here, and it brings a lot of exciting changes! Where extending the backoffice used to involve a mix of backend and frontend development, it’s now entirely a frontend-focused process. AngularJS has been replaced with modern TypeScript and Lit / Web Components, which I think is a fantastic step forward!
The new backoffice is also designed with modularity at its core. Every small component of the site is built to be modular, making it incredibly easy to extend and customize.
In this post, I’ll walk you through how to extend the new backoffice, using Pokémon as the subject for our example, by leveraging the PokeAPI. We’ll cover everything from setting up your extension to creating a custom dashboard, section complete with a menu, actions, and dynamic routes. We’ll also dive into building custom property editors. Let’s get started!
Setup
Umbraco
Set up and launch your new Umbraco 14 / 15 website by starting the project and creating a user using the following commands.
dotnet new install Umbraco.Templates
dotnet new umbraco --name MySite.Umbraco
cd MySite.Umbraco
dotnet run
Extension project
In the past, building extensions meant relying on AngularJS. However, with the new backoffice, you can now create extensions using any JavaScript framework capable of building to web components, such as React, Vue, Lit, and more. For this guide, we’ll use Lit with TypeScript to demonstrate how to get started.
Setting up a project for extensions is straightforward, it’s essentially a standard Vite project. In this example, I’m using Node.js version v20.18.0.
cd ../
npm create vite@latest MySite.BackofficeExtension
Next, select the package name, framework, and variant you want to use. Choose Lit as the framework and TypeScript as the variant.
√ Package name: ... mysite-backofficeextension
√ Select a framework: » Lit
√ Select a variant: » TypeScript
Complete the setup by adding the Umbraco CMS npm backoffice package. In this example, I’m using version 14.3.1.
npm i && npm install -D @umbraco-cms/backoffice@14.3.1
To set up the build configuration for your Vite project, create a vite.config.ts file in the root directory. This file defines critical settings for the build process, including specifying the entry file path, the output directory, and rollup options to manage dependencies. Here's an example configuration:
The entry property in lib specifies the main TypeScript file that serves as the entry point for your extension. In this example, it's set to src/index.ts.
Output Directory (outDir):
Defines where the built files will be stored. Here, the output is directed to the App_Plugins/BackofficeExtension directory within your Umbraco project.
Rollup Options:
Use rollupOptions.external to exclude dependencies (e.g., @umbraco modules) that should not be bundled, ensuring they are referenced as external resources.
Next, let’s create our entry file: src/index.ts. To keep things clean, remove all unused files from the src & public folder.
From here, you can build your project using the command npm run build, which utilizes your vite.config.ts.
Let’s begin by creating a umbraco-package.json file in the public folder of our Vite project. This file replaces the package.manifest from older versions and is used to define information about the extension.
This is also where we register our extensions, each defined with properties like name, alias, type, and more. To ensure our setup is working correctly, let’s add a dashboard to the extensions array.
{
//Other config here
"extensions": [
{
"type": "dashboard",
"alias": "My.Dashboard.MyExtension",
"name": "My Dashboard",
"meta": {
"label": "My Dashboard",
"pathname": "my-dashboard"
}
}
]
}
umbraco-package.json
When we run this and log into the backoffice, we’ll see our new dashboard tab appear. It doesn’t have any functionality yet, but don’t worry, that’s completely normal!
Entry point
To avoid writing a lot of JSON to register all of our extensions, we’ll use an entryPoint extension. This allows us to register all extensions using TypeScript through our entryPoint.
The entryPoint extension type enables JavaScript code to execute during backoffice startup. It points to a single JavaScript file that loads and runs when the Backoffice starts, essentially serving as the entry point for a package or extension.
Update our index.ts file to set up the entryPoint logic.
Let's update the umbraco-package.json file to register the entryPoint extension. This extension must be of type "entryPoint", and the js property should point to the path of the output bundle generated by our Vite build.
Now, we can add the same dashboard, but this time using TypeScript. To achieve this, we’ll create a new manifest file in our project and define our typed extension manifest.
I recommend always defining the alias of a extension as a constant. This practice allows you to reuse the alias in other parts of your project, reducing errors and making your code more maintainable.
Next, we’ll register our extension in the index file. We can do this by adding the objects to the manifests array.
//imports here
const manifests: Array<ManifestTypes> = [
//Add manifests to register here
DashboardManifest
];
//export here
/src/index.ts
Great! After building our Vite project and starting the Umbraco project, we can see that the new dashboard is now successfully registered.
However, as you navigate to other sections, you’ll quickly notice that our new dashboard is now visible across all sections.
We can resolve this by adding conditions to the dashboard extension to control where it is displayed. For instance, we can restrict the dashboard to only show in the content section.
This can be achieved by adding a conditions property to the extension, where you define the alias and the matching value.
In this case, to restrict the dashboard to the content section, we’ll use the Umb.Condition.SectionAlias alias and set the match property to the alias of the section where we want the dashboard to appear.
Now that our project is successfully set up, we can start working on our customizations.
Setting up a section
Let’s begin by adding a custom section for our extension. To do this, we’ll need to create a ManifestSection.
First, we’ll define a constant for the alias of our section. This allows us to easily reuse the alias later in the project. After that, we’ll register the section extension to ensure it’s recognized by Umbraco.
//export this because we will need it later!
export const SECTION_ALAIS = 'MySite.PokedexSection';
export const SectionManifest: ManifestSection = {
type: 'section',
alias: SECTION_ALAIS,
name: 'Pokedex Section',
weight: 10,
meta: {
label: 'Pokedex',
pathname: 'pokedex'
}
};
Tip
Don’t forget to register this manifest using the entrypoint!
Upon startup, you'll notice that the section tab isn’t visible yet. Just like in the old backoffice, we need to grant access to specific user groups for the section. Once that’s done, simply refresh the page, and you'll see the new section tab!
Adding a sidebar app with menu
Next, we’ll add a sidebar with a menu to display an overview of all the Pokémon.
As mentioned, we’ll be using the PokeAPI for this. I’ll create two types to parse the response from the Pokémon endpoint. Additionally, I’ll set up a service to access the API, which will include a GetPokemon method that returns an array of Pokemon.
In the new backoffice, this part of the section is referred to as the "Sidebar":
Extending the sidebar
To extend the sidebar, we can use Sidebar Apps. The sidebar app offers various types, and for this example, we’ll use menuWithEntityActions since we want to add a menu along with entity actions. To add a menu to the sidebar, we’ll need a Section Sidebar App, Menu, and Menu Item extension.
We’ll use the Menu Item component to fetch the data and render the menu items. For this, we’ll create a Lit element that implements UmbMenuItemElement. For more details about the menu-item component, check out the uui library.
Here’s a simple example of how to set up the menu items element to render a menu item for each Pokémon using Lit's functionalities:
const elementName = 'pokedex-menu-items';
@customElement(elementName)
class PokedexMenuItems extends UmbLitElement implements UmbMenuItemElement {
@state()
private _items: Pokemon[] = []; // Store fetched items
@state()
private _loading: boolean = true; // Track loading state
@state()
private _error: string | null = null; // Track any errors
constructor() {
super();
this.fetchPokemon(); // Start fetching on component load
}
// Fetch tree items
async fetchPokemon() {
try {
this._loading = true;
this._items = (await PokemonService.GetPokemon()); // Fetch root-level items
} catch (e) {
this._error = 'Error fetching items';
} finally {
this._loading = false;
}
}
// Render items
renderItems(items: Pokemon[]): TemplateResult {
return html`
${items.map(element => html`
<uui-menu-item label="${element.name}">
</uui-menu-item>
`)}
`;
}
// Main render function
render() {
//Showing loading state
if (this._loading) {
return html`<uui-loader></uui-loader>`;
}
//Showing error state
if (this._error) {
return html`<uui-menu-item active disabled label="Could not load pokemon!">
</uui-menu-item>`;
}
// Render items if loading is done and no error occurred
return html`${this.renderItems(this._items)}`;
}
}
//Export and declare the element
export { PokedexMenuItems as element };
declare global {
interface HTMLElementTagNameMap {
[elementName]: PokedexMenuItems;
}
}
src/section/menu/menu-items.ts
Now that we have our menu items component, we can add the manifests and link our element to the menu item manifest. We’ll then link the menu items Lit element to the element property of the ManifestMenuItem.
For this, we need a ManifestSectionSidebarApp, ManifestMenu, and as mentioned, our ManifestMenuItem:
When we start our Umbraco project and navigate to the Pokedex section, we can now see our newly added menu displaying the Pokémon from the Pokémon endpoint!
Adding dynamic routes
Suppose we want to create screens to access specific information about Pokémon, such as:
General Info
Moves
To achieve this, we can use a SectionView to manage the routing logic and render the appropriate components.
This image shows the component we refer to as a section view.
For our SectionView, we can create an element that extends UmbLitElement:
import { UmbLitElement } from "@umbraco-cms/backoffice/lit-element";
import { html, customElement } from '@umbraco-cms/backoffice/external/lit';
const element = "pokedex-sectionview";
@customElement(element)
export class PokedexSectionView extends UmbLitElement {
render() {
return html`
<h2>Test</h2>
`
}
}
export default PokedexSectionView;
declare global {
interface HTMLElementTagNameMap {
[element]: PokedexSectionView;
}
}
src/section/sectionView/manifest.ts
Let’s test this by creating and registering the manifest. For this, we’ll need a ManifestSectionView.
We can then link our section view component to the element property of the extension manifest.
After building the project and navigating to our section, we can see the newly created section view being rendered!
Next, let’s add routes to display the desired data. We’ll start by creating two Lit elements (similar to the one we made for the SectionView): one for the root, one for general info, and one for moves. These will be placed in /src/section/sectionView/routes.
By using the @property decorator, we can define properties for the elements. In this example, we only need an id property.
const element = "pokedex-moves";
@customElement(element)
export class Moves extends UmbLitElement{
@property({ type: String })
public idProp!: string;
protected render = () => {
return html`<div>
//Code for rendering moves here
</div>`;
}
}
export default Moves;
declare global {
interface HTMLElementTagNameMap {
[element]: Moves;
}
}
/src/section/sectionView/routes/moves.ts
We’ll render an unordered list with all the moves from the API and set up the routing. In the root component, we can add some basic content for the section’s root.
Take a look at the elements I used in this example here.
To configure the routing, we’ll use UmbRoute. Each route will require a path and a component, and we’ll use the setup property to pass any necessary properties. Additionally, we can include a redirect route for empty paths and a handler for routes that aren’t found.
When navigating to the Pokédex section, we see the root information for our section. Updating the URL to /moves/1 displays the dynamic text as expected. Now we could use the API service to fetch the data end render the base information.
To enable routing, we can add a href attribute to the menu items. This will render our general information component when the item is clicked.
The next step is to add an entity action that navigates to our "moves" route.
To begin, create a new element that extends UmbEntityActionBase. This element will contain the logic executed when the entity action button is clicked.
In this example, we’ll extract the id from the arguments and use it to navigate to the "moves" route associated with that ID.
export class ShowMoves extends UmbEntityActionBase<ManifestEntityActionDefaultKind> {
override async execute() {
let id = parseInt(this.args.unique);
window.history.pushState({}, '', `section/pokedex/view/pokemon/moves/${id}`);
}
}
export default ShowMoves;
src/section/menu/entityActions/show-moves.api.ts
To register the entity action, use ManifestEntityAction and link your custom element via the api property. You can also define an icon for the action button.
To control which types of entities the action applies to, use the forEntityTypes property.
To render the action buttons, place an umb-entity-actions-bundle element within the actions slot of the menu-item element and specify the entityType. Additionally, you can pass parameters to customize its behavior.
When we run this, we can see our bird icon button, which navigates to the "moves" route when clicked.
Adding Modals to Enhance Functionality
The final topic I want to discuss in this post is modals. Modals are useful for displaying additional information or requesting confirmation for an action.
Modals can function as either a dialog or a sidebar. For more details, check out the documentation.
So far, we’ve displayed the Pokémon’s general information and moves. Now, let’s use a modal to showcase the Pokémon’s stats.
To set up a modal, we’ll need the following:
ManifestModal: Used for registration.
UmbModalToken: Defines the modal with a value type and additional parameters, such as size.
Web Component: In this case, I’m using a LitElement that extends UmbModalBaseElement.
Let’s begin by defining the value type for the modal. This will specify the model that the modal requires.
export interface StatsModalType {
id: number;
}
src/section/menu/stats-modal/stats-modal.type.ts
Next, we need to configure the modal token. This defines the modal and links it to a specific value type and alias.
Additionally, we can specify the modal’s size and type. For instance, a dialog appears as a popup, while a sidebar displays as a popover on the side.
For your reference: the size parameter is applicable only to dialog.
Finally, let’s create the element that will display the data and register the modal. This element will appear within the modal and handle fetching the necessary data.
We’ll now add functionality to display the modal when the entity action is clicked by creating an element that extends the UmbEntityActionBase. In this element, we can consume the UMB_MODAL_MANAGER_CONTEXT to access the modalManager and open a modal using its token, passing the value type data to this method.
For more detailed information about the Context API, be sure to explore the documentation!
export class ShowStats extends UmbEntityActionBase<ManifestEntityActionDefaultKind> {
override async execute() {
const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
let id = 1;
modalManager.open(this._host, STATS_MODAL, {
data: {
id: id
},
});
}
}
export default ShowStats;
src/section/menu/entityActions/show-stats.api.ts
To complete the modal setup, we need an entity action manifest to link the entity type and assigning an icon.
Clicking the new entity action will open a modal displaying the stats of the selected Pokémon.
Conclusion
I hope this post provided valuable insights into setting up an extension for the new backoffice. If you have any questions, suggestions, or would like to discuss further, feel free to connect with me (for example, on LinkedIn)!
For more detailed guidance, check out the official documentation or explore other related posts on this topic. I'd love to hear about your experiences, so feel free to share what you’ve learned!