My 10 UmbraCommandments
At the Umbraco meetup in Odense on September 2, 2026, Kaspar Boel Kjeldsen gave a talk called The 10 Umbracommandments. The title made me wonder what my own list would contain.
I already had most of the answer. Over the years, my colleagues and I have written down the conventions we use when building Umbraco sites. Some came from upgrades that took more work than they should have. Others came from watching editors and developers navigate projects that had grown without enough structure.
These are not official Umbraco rules. Umbraco often allows several valid approaches. These are the defaults I have settled on after building and upgrading a lot of sites, mostly because they make the next decision easier to predict.
There is an old saying about sharing apples and ideas. If you and I exchange an apple, we still have one apple each. If we exchange an idea, we both leave with two ideas. I do not expect everyone to agree with all ten commandments. Comparing the things we each consider obviously correct is the interesting part.
The examples below use Umbraco 17 as their starting point.
1. Treat Program.cs as Umbraco’s property
Umbraco supports registering dependencies directly in Program.cs, through an IComposer, or through extension methods. I still treat Program.cs as if it belongs to Umbraco.
That rule comes from years of major upgrades. The generated host can change between versions. If a project has filled Program.cs with local registrations and startup logic, it becomes harder to compare the file with a fresh project and work out which changes belong to Umbraco and which belong to the site.
I put my registrations in composers by default. This keeps local code together and leaves the application entry point close to the one Umbraco generates. The dependency injection documentation treats both approaches as valid. My choice is about reducing upgrade work, not working around a limitation in Umbraco.
Sometimes a change really does belong in Program.cs. When that happens, I keep it small and add a clear comment so it is easy to find during the next upgrade.
2. Use the default MVC route until you need more
Umbraco already routes normal frontend requests through its RenderController. An ordinary content page does not need a custom controller just to return a view.
I keep presentation logic in Razor and use the default route for as long as it fits. Moving every page through a custom controller adds files, registrations, and another place to look without necessarily making the project easier to understand.
A product detail page can be a good exception. The Umbraco document may contain the editorial content, while prices, stock, and product specifications come from a PIM. A custom controller can fetch that data and build a viewmodel that combines both sources. It also gives me a useful boundary for testing the model generation without rendering the view.
Other real needs include server-side paging, special security rules, or route-specific business logic. The custom controller documentation lists similar cases.
The test is simple. I should be able to name the job the controller performs. “This page exists” is not enough.
3. Give every site its own root
Every website I build gets a SiteNode at the root of the Content tree. The frontpage and the other pages sit below it.
The site node owns the hostname, cultures, global settings, navigation settings, and the rules for what editors may create below it. The frontpage remains a page. Replacing it does not require moving the rest of the site, and adding another site follows the same structure.
I do this even for a small single-site project. It adds one level to the tree, but the hierarchy models the website more accurately. An About page belongs to the site. It is not conceptually a child of the frontpage.
My article about the ultimate Umbraco site structure covers the full routing setup with a URL provider and content finder. There is also a simpler version. A Content Picker property with the alias umbracoInternalRedirectId can make the site root render the selected frontpage, and umbracoUrlAlias can give the frontpage / as an additional URL.
That simple version may be enough when duplicate URLs are acceptable. If canonical URLs matter, the custom inbound and outbound routing in the longer article avoids exposing both / and /home for the same page.
4. Make aliases explain the content type’s role
I use aliases as a small classification system:
- A type with its own frontend page ends in
Page. - A page that contains a list of children ends in
ListPage. - A non-rendered item in the Content tree ends in
Node. - A node that contains a list of children ends in
ListNode. - A dedicated composition ends in
Composition. - An element used for structured content, such as the items in a navigation Block List, ends in
Element. - An element rendered as part of a page composer ends in
Block.
For example, a news archive and its articles become NewsArticleListPage and NewsArticlePage. An employee archive used as structured data becomes EmployeeListNode, with EmployeeNode items below it.
These suffixes are not enforced by Umbraco. They are a team convention that we check in review. They let me understand the intended role of a generated model or a schema file without opening the backoffice. A NavigationLinkElement holds structured data. A GalleryBlock is expected to have a matching view, typically under Views/Partials/BlockList/Components.
Most of the sites I build have Danish editors, so the names they see in the backoffice should usually be Danish. The code and names derived from the schema, including aliases and generated models, should be English.
I therefore create a new type with its English name, set its final alias, and save it before changing the display name to Danish. This matters when creating a Document Type with Template because Umbraco creates the related template at the same time. The editor gets Nyhedsoversigt, while the code gets NewsArticleListPage.
Umbraco 18 note: The new Library gives reusable, non-routable content a proper home outside the Content tree. Some types I would place in the Content tree as data nodes in Umbraco 17 may belong in the Library instead. I do not have enough experience with Library to settle on a new suffix, and my current inclination is to keep
Node.SiteNodestill belongs in Content because it defines the site’s place in the content hierarchy.
Architectural tests could eventually check these rules automatically. I have not explored that far yet, so today they remain a reviewed convention.
5. Use icons and colors as a language
An editor looking at the Content tree sees the names of the content items, not their content type names. The icon is the immediate clue that two similarly named items serve different roles. I therefore use icons and colors consistently.
Across our projects at Ecreo, purple is reserved for compositions and blue-grey for data nodes. Both colors are available in the new backoffice, so we can keep the convention when moving a project beyond Umbraco 13. Using the same colors across projects makes an unfamiliar project quicker to understand.
Pages, blocks, and elements can use the other available colors. Once developers and editors know the convention, they can scan a crowded tree without opening every item.
The color picker still gives us fewer useful choices than it did in Umbraco 13. In a discussion about improving the icon picker, I pointed out that only black, blue, and red had sufficient contrast against a white background at the time. I suggested allowing projects to load their own icon colors, or at least adding teal, purple, and orange to the standard selection. The discussion also covers colors for light and dark themes, and whether the built-in palette should guarantee WCAG AA contrast.
When the standard icons do not fit, I am happy to add a custom SVG icon using the current icon extension mechanism. It works, but I would like a simpler way to add one or two icons for a specific project without building a complete extension around them.
6. Decide compositions before content exists
Compositions are useful when several content types genuinely share the same properties. I use them for deliberate capabilities such as navigation settings or SEO fields, and I create document types specifically for that purpose.
I do not turn an ordinary page type into a composition just because it happens to contain a useful property. That couples two roles which may change independently later.
The best time to decide this is before editors create content. Property values in the database refer to a specific property type ID. If I remove a property from one content type and recreate the same alias on a new composition, the new property gets a different identity. Existing values do not automatically follow it.
SQL scripts for moving that data have circulated in the community, but I consider direct surgery on property data risky. I would rather spend more time on the schema before content exists than turn a tidy-up into a database migration later.
Compositions also need room to fit into their consumers. I set explicit sort orders for their tabs, groups, and properties, often using values such as 100, 200, and 300. The gaps make it easier for another content type to place its own fields around the composed ones.
7. Build with blocks, but do not turn everything into one
Block List is my usual tool for flexible page content. I look for self-contained parts of the design such as content sections, banners, and galleries, then model each one as an element that editors can add where it makes sense.
This gives editors freedom inside the design instead of making them responsible for the design. The available block types, their settings, and the data type configuration still define the boundaries.
The temptation is to put every field into the page composer. A fixed hero on a frontpage is a useful counterexample. If it must exist exactly once, cannot move, and will never be used as an optional section elsewhere, it should not appear among the blocks that editors can add and remove freely.
Direct properties on the frontpage type are one option. Before Umbraco 17, I could get much the same result with a Block List configured with one available block type, a minimum of one, and a maximum of one. Umbraco 17 introduced the dedicated Single Block property editor for this use case. It keeps the hero fields grouped in an element type without asking the editor whether the required hero should exist.
8. Keep the schema understandable
The KISS principle applies to the backoffice too. Editors should not have to scan more tabs than the task needs, and developers should not have to search an unstructured Document Types tree.
My starting point is one Content tab and one Settings tab. In my experience, that is enough for roughly 80 percent of content types. It is a rule of thumb, not a measured target. A third tab is fine when it represents a real part of the editor’s work. A tab containing one isolated property usually makes the interface harder to scan.
I apply the same thinking to the Document Types tree. Folders such as Pages, Compositions, Blocks, and Data are often enough. The exact folders depend on the project, but leaving every type in one long list is rarely the clearest answer.
These choices help during onboarding as much as they help editors. A new developer can predict where to find a block, what the project’s icon colors mean, and which tab contains technical settings before anyone explains the entire project.
9. Reuse data types until the purpose changes
Data Type trees get messy quickly because creating a new configuration is easy. I try to reuse a general data type until the new use case has a meaningful difference.
URL pickers are the obvious example. Most projects can start with one picker for a single link and one for multiple links. A third copy with the same limits and behaviour only gives the team another item to name and maintain.
A new data type is justified when its configuration expresses a different editorial job. It may have another maximum, different validation, or a deliberately narrow choice. An Employee Picker, for example, can start at the employee archive and only allow EmployeeNode items. That is easier for the editor and prevents invalid selections.
I also reuse proven schema between projects rather than recreating every baseline type by hand. Depending on the project, that can mean copying the relevant Umbraco Deploy .uda files or uSync files. Dependencies still matter, so I move a coherent group of document types, data types, and templates instead of treating one schema file as self-contained. The Umbraco Deploy import and export documentation and uSync import documentation explain the two workflows.
For related projects hosted on Umbraco Cloud, Baselines provide a more deliberate version of the same idea. A child project starts from the baseline’s code and schema, and later baseline changes can be pushed to connected child projects.
Reuse is the default. A concrete editorial difference is the reason to branch.
10. Override as little of Umbraco Forms as possible
My first choice for styling Umbraco Forms is its default markup plus project CSS. This keeps the project close to the version of Forms it runs and leaves Umbraco free to fix or adjust its own theme during upgrades.
Some designs need markup that CSS cannot provide. Sometimes a targeted override in the local default theme folder is enough. A custom theme makes sense when a particular form or group of forms needs different markup. Either way, I only override the views that need to differ. Forms looks in the selected theme first and falls back to the default theme for the rest. Copying every view turns an intentional exception into a fork that the project must maintain.
The Forms theme documentation also warns that its conditional JavaScript relies on specific CSS classes. Adding classes is fine. Removing the expected ones can break behaviour that is not obvious from the markup alone.
Custom field types need a default frontend view. I put that view in Views/Partials/Forms/Themes/default/FieldTypes/FieldType.[alias].cshtml, which lets custom themes inherit it unless they need their own version.
The rule is not that Forms must look untouched. The rule is that every override should have a reason.
Final thoughts
We use these commandments during onboarding, content modelling, and code review. They give a team a shared starting point, not a checklist that every project must pass without exception.
A convention earns its place when it prevents a problem or removes an unnecessary decision. When a project has a concrete reason to break one, I break it. I just want the reason to be clear enough that the next developer does not have to reverse-engineer it.
Kaspar’s talk gave me the name for this list. His commandments may be different from mine, and yours may be different again. That is the useful part. Sharing the list does not leave either of us with fewer ideas.