> ## Documentation Index
>
> Fetch the complete documentation index at: https://superpowers.frontis.nl/llms.txt
> Use this file to discover all available pages before exploring further.

---

title: Umbraco development
summary: Frontis Umbraco development guidance for coding agents working on projects based on the Frontis Umbraco base project (dotnet new frontisumbraco), covering architecture, content retrieval, Block List, adapter packages and testing.
order: 55

---

# Umbraco development

Use this guide when working on a Frontis Umbraco project.

This document is written for coding agents. It explains how to make safe, consistent changes in projects that were created from the Frontis Umbraco base project.

## When this guide applies

This guide applies ONLY when the repository is an Umbraco project.

A repository is a Frontis Umbraco project when one or more of the following is true:

- `Directory.Packages.props` or a `.csproj` references `Umbraco.Cms`;
- the solution contains projects named `*.Web`, `*.Application`, `*.Application.Contracts`, `*.Domain`, `*.Domain.Shared` and `*.Generated.Models` in the Frontis Umbraco layout;
- the repository was created with `dotnet new frontisumbraco`.

Do NOT apply this guide to:

- ABP projects;
- Nuxt projects;
- custom .NET projects without Umbraco.

For generic .NET guidance, read the .NET development document. This document adds Umbraco-specific rules on top of it. When they conflict, this document wins for Umbraco projects.

## The Frontis Umbraco base project

Frontis maintains a base project for Umbraco websites. New Umbraco projects are NOT started by cloning a repository. They are created from the `Frontis.Umbraco.Templates` dotnet template:

```powershell
dotnet new install Frontis.Umbraco.Templates
dotnet new frontisumbraco -o [Projectname] --ApplicationName [ApplicationName] --DevopsProjectName [Projectname] --hostname [Hostname]
```

The base project lives in the Frontis Azure DevOps `Umbraco` project. The current major is Umbraco 17 on .NET 10.

Key properties of every project created from this template:

- ABP.io-inspired clean architecture with DDD layering;
- Central Package Version Management (`Directory.Packages.props`);
- package source mapping in `nuget.config`: `Frontis.*` packages come from the private Frontis Azure DevOps feed, everything else from nuget.org;
- one `appsettings.json` for all environments with pipeline/Bicep overrides; developer-specific values go into a gitignored `appsettings.Development.json` copied from `appsettings.template.json`;
- uSync for document type synchronization, with Husky.Net git hooks that trigger one-time imports after pull;
- Vite + TypeScript + Tailwind CSS frontend in the Web project;
- NUnit + NSubstitute for tests;
- multi-stage Azure DevOps YAML pipelines with Bicep IaC in `/deployment`.

## Source-of-truth priority

Use this priority order:

1. explicit user instruction;
2. local repository files (`AGENTS.md`, `.frontis/*`, `Examples/` folders in the solution);
3. this document and the Frontis Umbraco package catalog;
4. existing code style in the repository;
5. general Umbraco knowledge.

The `Examples/` folders inside the solution layers contain canonical example code (`ExampleAppService`, `ExampleUmbracoDomainService`, mapper examples). Read them before writing new services. They are newer than most written documentation.

Never invent Frontis Umbraco conventions from memory.

## Architecture and layering

The solution follows this dependency flow:

```text
Web -> Application -> (Application.Contracts + Domain) -> (Domain.Shared + Generated.Models)
```

Where code belongs:

| Code | Project |
| --- | --- |
| DTOs returned to the Web layer | `{App}.Application.Contracts` |
| Application service implementations | `{App}.Application` |
| Domain models (POCOs) | `{App}.Domain` |
| Constants | `{App}.Domain.Shared` |
| ModelsBuilder output | `{App}.Generated.Models` (never hand-edited) |
| Controllers, ViewComponents, views, middleware | `{App}.Web` |

Rules:

- **do NOT create interfaces for Umbraco-backed services** — integration tests run against the real Umbraco instance, not mocks; only create interfaces for genuinely-mocked external seams (typed HTTP clients);
- only the Application and Web projects reference `Generated.Models`; Application uses them for mapping, Web for views;
- application services reading Umbraco content should derive from `UmbracoContentReaderBase` and live in the Application project;
- mappers follow the naming convention: `Umbraco{Feature}ContentDomainMapper` (Umbraco→Domain) and `{Feature}DtoMapper` (Domain→DTO);
- controllers depend on application services directly (concrete classes), never bypass them to call Umbraco APIs;
- never return `IPublishedContent` or generated models from an API endpoint — return DTOs;
- organize code in feature folders (`Navigations`, `Localizations`, `Blogs`), not technical folders.

## Dependency injection

Services MUST be registered in the layer module builders, not in `Program.cs`:

- application services: `ApplicationModuleBuilder.AddApplicationModule(this IUmbracoBuilder)` in the Application project (register concrete classes with `AddTransient`, not interfaces);
- adapter package services: use `TryAddTransient` to allow consumer override.

`Program.cs` calls `AddApplicationModule()` plus the adapter package builders (`.AddNavigationModule()`, `.AddSearchModule()`, etc.).

Only register a service in `Program.cs` when the service is defined in the Web project itself.

Register notification handlers via `builder.AddNotificationHandler<TNotification, THandler>()` inside the module builders.

Bind options inside the module builders:

```csharp
builder.Services.Configure<MyFeatureOptions>(
    builder.Config.GetSection(MyFeatureOptions.SectionName));
```

Example application service registration (no interface):

```csharp
builder.Services.AddTransient<FaqAppService>();
builder.Services.AddTransient<NewsAppService>();
builder.Services.AddTransient<RootNodeAppService>();
```

## Retrieving Umbraco content

This is the most important Frontis Umbraco rule set.

Hard rules:

- NEVER use `UmbracoHelper`. It requires an HTTP context and does not work in unit tests, integration tests, or background jobs. Warn the user when they ask for it.
- NEVER use `IContentService` to read published content. It bypasses the published content cache and queries the database directly.
- `IContentService` is ONLY for creating, updating and deleting content, and for reading draft content.
- Reading published content is an **Application-layer** concern (Umbraco adapters), NOT a Domain concern — the Domain layer stays Umbraco-free.
- Umbraco-backed services have a single implementation and are never mocked (tests run against a real Umbraco instance), so do NOT hide them behind an interface — inject and register the **concrete** type. Only genuinely-mocked external seams (e.g. a typed HTTP API client) keep an interface.

All the Umbraco plumbing — ensuring a context, scoping the culture, multi-site root detection, tree traversal and preview — lives in the shared base class `UmbracoContentReaderBase`. Content readers/app services derive from it and stay one-liners; never re-implement this plumbing:

```csharp
public sealed class FaqAppService(
    IUmbracoContextAccessor umbracoContextAccessor,
    IUmbracoContextFactory umbracoContextFactory,
    IVariationContextAccessor variationContextAccessor,
    IDocumentNavigationQueryService documentNavigationQueryService)
    : UmbracoContentReaderBase(
        umbracoContextAccessor, umbracoContextFactory,
        variationContextAccessor, documentNavigationQueryService)
{
    // Explicit culture:
    public IReadOnlyList<FaqItemDto> GetFaqItems(string culture) =>
        Read(culture, () => GetAllOfType<FaqItem>().Select(x => x.ToFaqItemDto()).ToList());

    // Ambient (current-request) culture — no culture argument needed:
    public IReadOnlyList<FaqItemDto> GetFaqItems() =>
        Read(() => GetAllOfType<FaqItem>().Select(x => x.ToFaqItemDto()).ToList());

    // Preview / unpublished draft content:
    public IReadOnlyList<FaqItemDto> GetFaqItemsPreview(string culture) =>
        Read(culture, () => GetAllOfType<FaqItem>(preview: true).Select(x => x.ToFaqItemDto()).ToList());
}
```

What the base provides:

- `Read(culture, fn)` / `Read(fn)` — ensures an Umbraco context (works in tests, Hangfire, startup) and scopes the culture; the parameterless overload reads in the current (ambient) request culture;
- `GetAllOfType<TModel>()` / `GetAllOfType<TModel>(preview: true)` — every node of a generated model type below the site roots; the `preview` overload includes unpublished drafts;
- `GetById(key, preview)` and `GetChildrenOfType<TModel>(parent, preview)` — preview-aware single-node and typed-children reads;
- `GetSiteRoots()` — multi-site-aware root detection.

For multi-site root access, do NOT hand-roll `TryGetRootKeys()` + cast. Inject the concrete `RootNodeAppService` and use its intent-revealing accessors (each has an explicit-culture and an ambient-culture overload):

```csharp
var homepage = rootNodeAppService.GetHomepage();          // ambient culture
var hub      = rootNodeAppService.GetContentHub("nl-NL");  // explicit culture
var root     = rootNodeAppService.GetRootNode<MyRoot>();    // any generated root type
// TryGet* variants return null instead of throwing when the root is absent.
```

Mapping is the boundary that keeps the layers clean. Mappers are named after what they **output** and live per feature (no central `Mappers` folder):

- `{Feature}DomainMapper` — Umbraco generated model → Domain model (anti-corruption boundary into the Domain layer); lives under the feature's `Umbraco/Mappers/`;
- `{Feature}DtoMapper` — Domain model → DTO (the outward API boundary). In the simple, no-Domain case a single `{Feature}DtoMapper` maps the Umbraco model straight to the DTO;
- never return `IPublishedContent` or generated models from an API endpoint — return DTOs.

Other rules:

- cast `IPublishedContent` to the generated models (`.OfType<Homepage>()`) — never work with magic property aliases;
- `VariationContextScope` (in the Application project under `Umbraco/`) is the low-level culture scope the base uses internally; use it directly only in special cases outside a reader.

Follow the up-to-date examples in the repository you are working in: `src/{App}.Application/Umbraco/UmbracoContentReaderBase.cs`, the readers under `src/{App}.Application/Examples/`, and `src/{App}.Application/MultiSite/RootNodeAppService.cs`.

## Generated models and uSync

The solution uses Umbraco's embedded ModelsBuilder:

- in local development `appsettings.Development.json` (from `appsettings.template.json`) sets `ModelsMode: SourceCodeAuto` with `ModelsDirectory` pointing at the `Generated.Models` project;
- in `appsettings.json` (all deployed environments) `ModelsMode` is `Nothing` — models are compiled, not generated at runtime;
- models regenerate automatically while the site runs locally and a document type changes;
- never edit `*.generated.cs` files by hand.

Document types are versioned with uSync under `src/{App}.Web/uSync/`:

- changing a document type in the backoffice updates the uSync files; commit them together with the regenerated models;
- Husky.Net git hooks create `uSync/usync.once` after a pull/checkout that changed uSync files, which triggers a one-time import on the next site start;
- when document types and code get out of sync, start the site locally so ModelsBuilder regenerates before building dependent code.

Document type naming (aliases camelCase, generated models PascalCase):

```text
pages:        homepage, newsItem, searchPage
blocks:       blockList{Name} + blockList{Name}Settings
compositions: composition{Name}  (generates ICompositionX interfaces)
elements:     element{Name}
utility:      settings, robotsTxt, sitemapXml, errorPage404
```

## Block List architecture

Page content is composed from Block List blocks rendered by `Frontis.Umbraco.BlockList` (strongly typed ViewComponents with assembly-scanning auto-discovery, registered with `.AddFrontisBlocklist(...)`).

Block List work has its own detailed rules — backoffice tree placement, document type and tab rules, data type and content wiring, layouts, images and compositions. Before adding or changing any Block List block, follow the dedicated Block List guide:

```text
https://superpowers.frontis.nl/ai/blocklist.md
```

## Views and ViewComponents

- page views live flat in `Views/`, named exactly after the document type model (`Homepage.cshtml`); they inherit `FrontisViewBase<TModel>` and stay tiny — mostly `<vc:block-list>` invocations;
- there is NO `Partials` folder — every reusable fragment is a ViewComponent with `.cs`, optional `ViewModel.cs` and `.cshtml` colocated in one folder under `Components/{Area}/` (`Layout`, `Shared`, `SEO`, `StructuredData`, `Search`, `Overview`);
- invoke ViewComponents with the kebab-case tag helper syntax: `<vc:main-menu>`, `<vc:responsive-image>`;
- global page data (`CurrentPage`, `Settings`, `SearchPage`, `MainMenu`) is exposed via `GlobalDataActionFilterAttribute` and typed properties on `FrontisViewBase` — do not re-query it in views.

## Frontis adapter packages

Before building functionality yourself, check the Frontis package catalog:

```text
https://superpowers.frontis.nl/conventions/umbraco-packages.yaml
```

These `Frontis.*` NuGet packages come from the private Frontis feed and cover backoffice Entra ID login, Block List rendering, search, tag helpers, structured logging, MJML mails, member accounts, PDF generation, doc type building, Hangfire extensions, commerce, and more.

Rules:

- prefer an existing Frontis adapter package over custom code;
- prefer extending an adapter package over copying its code into the project;
- most packages register with a builder extension on `IUmbracoBuilder` in `Program.cs` (for example `.AddFrontisBlocklist(...)`, `.AddSearchModule()`, `.AddMjml()`) and are configured under the `Frontis:` section in `appsettings.json`;
- package versions follow the Umbraco major (`17.x`) or, for Umbraco-independent packages, the .NET major (`10.x`);
- keep versions in `Directory.Packages.props` (CPVM) — never add a `Version` attribute on a `PackageReference`;
- do not change `nuget.config` package source mappings.

When generic functionality is developed that other Frontis Umbraco projects can reuse, propose extracting it into a new adapter package in the base project repository instead of leaving it in the customer project.

## Configuration

- one `appsettings.json` for all environments; environment-specific values are overridden by the pipeline/Bicep through environment variables;
- there is NO `appsettings.test.json` or `appsettings.Production.json`;
- the only local override is the gitignored `appsettings.Development.json`, created from `appsettings.template.json`;
- when adding a new setting, add it to BOTH `appsettings.json` (with the deployed default or pipeline placeholder) and `appsettings.template.json` (with a working local value);
- never commit secrets; local secrets go in `appsettings.Development.json` or user secrets, deployed secrets come from the pipeline/Key Vault;
- adapter package settings live under the `Frontis:` configuration section.

## Testing

Test stack rules:

- use NUnit (Umbraco uses NUnit) and NSubstitute for mocking — never introduce xUnit, MSTest or Moq;
- integration tests build on the `Umbraco.Cms.Tests.Integration` package and extend the test base classes in the `{App}.TestBase` project;
- test projects follow `{App}.{Layer}.{Unit|Integration|Performance}.Tests`;
- performance tests use BenchmarkDotNet;
- integration tests reuse the Web project's `appsettings(.Development).json` and the real DI modules (`AddApplicationModule()` plus the adapter package builders) — do not build custom DI setups or test-specific appsettings;
- integration tests run against a real (uSync-seeded) database and are NOT run in parallel, so do not add test-specific DI overrides or per-test isolation helpers;
- in integration tests resolve services with `using var scope = CreateScope();` and `scope.ServiceProvider.GetRequiredService<T>()`.

Preferred workflow for new features (TDD):

1. write an integration test against the application service (concrete type);
2. implement the feature;
3. refine with unit tests;
4. add benchmarks only for performance-sensitive paths.

## Frontend

The Web project contains a Vite + TypeScript + Vue 3 + Tailwind CSS 4 frontend:

- sources in `src/{App}.Web/src/` (`js/main.ts`, `js/plugins/*`, `css/main.css`);
- run the dev server with `npm run dev` (port 5174, proxied by the site in Development);
- lint with `npm run lint`, format with `npm run format`;
- build output goes to `wwwroot/dist/` with hashed filenames; `Master.cshtml` switches between the Vite dev server and `dist` globs via `<environment>` tags;
- follow existing plugin structure when adding frontend behavior; styles use Tailwind utilities plus `fr-` prefixed project classes.

## Running and verifying locally

From `src/{App}.Web/`:

```powershell
dotnet watch --non-interactive   # backend with hot reload
npm run dev                      # Vite dev server
```

The local site runs on `https://l-{hostname}:5001/`. SSL binding uses the `FRONTIS_SSL_THUMBPRINT` environment variable; the hostname must be in the hosts file (FrontisMate does this during init).

Before claiming completion, run from the repository root:

```powershell
dotnet build ./{App}.app.sln
dotnet test ./{App}.app.sln
```

and `npm run lint` in the Web project when frontend files changed.

The repository treats nullable warnings as errors. Do not suppress them; fix the nullability.

Useful local endpoints: `/health-status` (health checks), `/.fcp` (Frontis Control Panel), `/umbraco` (backoffice).

## MCP server

Umbraco 17 projects ship the Umbraco CMS Developer MCP Server (`.vscode/mcp.json`), giving agents access to the Umbraco Management API (document types, data types, documents, media) on the local development site.

- the MCP API user is seeded automatically on first Development startup;
- the developer configures the `Frontis:Mcp` section in `appsettings.Development.json`;
- prefer the MCP server over guessing document type structures;
- NEVER connect the MCP server to a test, acceptance or production environment.

## Security

Umbraco-specific security rules, in addition to the general security document:

- backoffice Entra ID login lives in `Frontis.Umbraco.BackofficeAuthentication` — do not modify authentication flows in the project itself;
- non-production environments are protected by `Frontis.Web.NonLiveAccess` — do not weaken or remove it;
- security headers and CSP come from `Frontis.Umbraco.WebCommon` (`UseSecurityPolicy()`, `ContentSecurityPolicy` section) — extend the configuration, do not disable the middleware;
- never expose draft content or the Management API publicly;
- never commit the uSync.Complete license key or other secrets.

## Anti-patterns

Avoid:

- using `UmbracoHelper` anywhere;
- reading published content through `IContentService`;
- registering services in `Program.cs` that belong in a module builder;
- returning `IPublishedContent` or generated models from controllers;
- editing `*.generated.cs` files;
- magic string property aliases instead of generated models;
- creating partial views instead of ViewComponents;
- block components that do not follow the `BlockList{Name}` naming;
- introducing xUnit, Moq or test-specific appsettings;
- adding package versions outside `Directory.Packages.props`;
- building custom functionality that an existing Frontis adapter package already provides;
- upgrading the Umbraco major version as part of unrelated work.

## Pull request checklist

Before opening or finishing a PR in a Frontis Umbraco project:

```text
[ ] content is read via UmbracoContentReaderBase, not UmbracoHelper/IContentService
[ ] Umbraco-backed services are concrete classes (no interface), registered in the correct module builder
[ ] code placed in the correct layer (Contracts/Application/Domain/Web)
[ ] DTOs returned from controllers, mapped via the Domain/Dto mappers
[ ] new blocks follow the BlockList naming and folder structure
[ ] uSync files and regenerated models committed together
[ ] new settings added to both appsettings.json and appsettings.template.json
[ ] NUnit + NSubstitute tests added or updated; dotnet test ran and passed
[ ] package versions only in Directory.Packages.props
[ ] no secrets committed
[ ] npm run lint passed when frontend files changed
```
