Umbraco renders every content page through ASP.NET Core MVC: a request is matched to a content
node, routed to a controller (Umbraco's RenderController unless you hijack it), and rendered by
the node's Template, a Razor view.
Pick the building block
| Need | Use | Reference |
|---|---|---|
| Markup for a Document Type's pages, shared page shell | Template + master layout | views-and-partials.md |
| Reusable markup that only formats the model it's given | Partial view | views-and-partials.md |
| Reusable widget with its own logic or injected services | View component | view-components.md |
| Page needs data that isn't on the node (external API, paging, per-request logic), or a different view per template | Route hijacking: RenderController + view model |
route-hijacking.md |
| Accept a form POST from a content page | Surface controller | surface-controllers.md |
| JSON / REST endpoint | Not a templating concern: plain ASP.NET Core controller | Other controller types in surface-controllers.md |
Before writing any code, open the reference for every block you use and start from its asset.
The Umbraco 17 traps that break these pages are covered only there. For example, a hijacked
Index overload throws AmbiguousMatchException, and an *Async surface action name doesn't route.
Code written from this page alone will hit them.
How to decide
Use the lightest block that does the job: template → partial → view component → hijacked
controller. Querying other content doesn't need a hijack: a template or view component can do it.
A single page often combines several blocks:
assets/productPage.cshtml is a hijacked template that invokes a view
component and renders a form partial.
A new template renders only once it's registered in Umbraco and assigned to the Document Type, otherwise the page 404s: see step 3 of views-and-partials.md.
Finish every building block with a build and a request to a page that exercises it. Check
Umbraco:CMS:ModelsBuilder:ModelsMode first:
NothingorSourceCode*: build withdotnet build -p:RazorCompileOnBuild=true. The stock Umbraco csproj setsRazorCompileOnBuild=false, so a plaindotnet buildnever compiles a view and passes even when they're broken.InMemoryAuto(Umbraco's default): the generated models exist only at runtime, so that build fails with CS0246 on a healthy site. A plaindotnet buildplus the page request is the check. Don't change the user'sModelsModeor rewrite their views to make the Razor build pass.
If no runnable project is available, say so rather than claiming it works.
Version compatibility
Targets Umbraco 17. The assets/ compile and render on Umbraco.Cms 17.5.3. On Umbraco 18,
PublishedContentWrapped takes only (IPublishedContent), so drop the fallback argument from the
view model.
Several snippets on the Umbraco 17 docs pages are v8-era and do not compile or run on 17. References whose docs pages have broken snippets list them under Docs corrections. Trust the assets over the docs where they disagree.
Best practices
- Views render, they don't fetch. No
IContentService/IMediaServicein a view. Move logic into a view component or a hijacked controller. For performance traps (deep traversal, repeatedRoot()), see the sibling skillumbraco-common-pitfalls. - Async rendering only:
<partial>,await Html.PartialAsync,await Component.InvokeAsync. NeverHtml.Partial/Html.RenderPartial, which are sync-over-async and can deadlock (analyzer MVC1000). - Typed values. Use
@(Model.Value<T>("alias")), or the generated ModelsBuilder property. The@(...)is required: in a bare@Model.Value<T>(...)Razor reads<T>as an HTML tag (CS1503). Rich text converts toIHtmlEncodedString, so render it directly. NeverHtml.Rawan editor string. - Keep the master layout generically typed (
UmbracoViewPageor a shared interface, notUmbracoViewPage<Home>), or a hijacked page with a custom view model will throw when the layout binds.
Validation
Objective assertions live in evals/evals.json; run them with
umbraco-skill-evaluator. Every asset is Asserted by the runtime gate
(examples/render-pipeline/, blank host): the hijacked route, query binding, property fallback,
the view component and both form-post paths are checked over HTTP.