Development of Custom Umbraco Template
Template in Umbraco is Razor View (.cshtml). Each content type (Document Type) has matching view with same alias. Controller not needed — Umbraco creates Render Controller by default — or write custom for logic.
Base Layout
@* Views/Shared/_Layout.cshtml *@
@using Umbraco.Cms.Web.Common.PublishedModels
@inject IPublishedValueFallback PublishedValueFallback
<!DOCTYPE html>
<html lang="@System.Threading.Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@ViewData["Title"] – @ViewBag.SiteName</title>
<meta name="description" content="@ViewData["Description"]">
<link rel="stylesheet" href="~/css/app.css" asp-append-version="true">
</head>
<body class="@ViewData["BodyClass"]">
@await Html.PartialAsync("_Navigation")
<main>@RenderBody()</main>
@await Html.PartialAsync("_Footer")
<script src="~/js/app.js" asp-append-version="true" defer></script>
@RenderSection("scripts", required: false)
</body>
</html>
Strongly-Typed Article Template
@* Views/Article.cshtml *@
@using Umbraco.Cms.Web.Common.PublishedModels
@model ArticleViewModel
@{
Layout = "_Layout.cshtml";
ViewData["Title"] = Model.SeoTitle ?? Model.Title;
ViewData["Description"] = Model.SeoDescription;
ViewData["BodyClass"] = "page-article";
}
<article class="container article">
<header class="article__header">
@if (Model.CoverImage != null)
{
<figure class="article__cover">
<img
src="@Model.CoverImage.GetCropUrl(width: 1200, height: 630)"
srcset="@Model.CoverImage.GetCropUrl(600) 600w,
@Model.CoverImage.GetCropUrl(1200) 1200w"
sizes="(max-width: 768px) 100vw, 1200px"
alt="@(Model.CoverImage.Name)"
loading="eager"
>
</figure>
}
<div class="article__meta">
@if (Model.Category != null)
{
<a href="@Model.Category.Url" class="badge">
@Model.Category.Name
</a>
}
<h1 class="article__title">@Model.Title</h1>
<time datetime="@Model.PublishDate?.ToString("yyyy-MM-dd")">
@Model.PublishDate?.ToString("dd MMMM yyyy")
</time>
</div>
</header>
<div class="article__body prose">
@Html.Raw(Model.BodyHtml)
</div>
@if (Model.RelatedArticles.Any())
{
<aside class="related">
<h2>Related Articles</h2>
<div class="related__grid">
@foreach (var related in Model.RelatedArticles)
{
@await Html.PartialAsync("_ArticleCard", related)
}
</div>
</aside>
}
</article>
Controller with Data
// Controllers/ArticleController.cs
public class ArticleController : RenderController
{
private readonly IUmbracoMapper _mapper;
private readonly IRelatedContentService _relatedService;
public ArticleController(
ILogger<ArticleController> logger,
ICompositeViewEngine viewEngine,
IUmbracoContextAccessor umbracoContextAccessor,
IUmbracoMapper mapper,
IRelatedContentService relatedService)
: base(logger, viewEngine, umbracoContextAccessor)
{
_mapper = mapper;
_relatedService = relatedService;
}
public override IActionResult Index()
{
if (CurrentPage is not Article article)
return NotFound();
var model = new ArticleViewModel
{
Title = article.Title,
BodyHtml = article.Body?.ToString(),
PublishDate = article.PublishDate,
SeoTitle = article.SeoTitle,
SeoDescription = article.SeoDescription,
CoverImage = article.CoverImage?.First() as IPublishedContent,
Category = article.ArticleCategory?.First() as IPublishedContent,
RelatedArticles = _relatedService.GetRelated(article, 3),
};
return CurrentTemplate(model);
}
}
Block List and Block Grid
Umbraco 9+ uses Block List and Block Grid instead of Grid Editor:
@* Render Block List *@
@foreach (var block in Model.Content.Value<BlockListModel>("pageBlocks") ?? [])
{
var alias = block.Content.ContentType.Alias;
@await Html.PartialAsync($"Blocks/_{alias}", block)
}
Partial for heroBlock:
@* Views/Partials/Blocks/_heroBlock.cshtml *@
@model Umbraco.Cms.Core.Models.Blocks.BlockListItem
@{
var content = (HeroBlock)model.Content;
var settings = model.Settings as HeroBlockSettings;
}
<section class="hero hero--@settings?.Theme">
<div class="hero__inner">
<h1>@content.Heading</h1>
@if (!string.IsNullOrEmpty(content.Subheading))
{
<p class="hero__sub">@content.Subheading</p>
}
@if (content.CtaLink != null)
{
<a href="@content.CtaLink.Url"
target="@content.CtaLink.Target"
class="btn btn--primary">
@content.CtaLink.Name
</a>
}
</div>
@if (content.BackgroundImage?.First() is IPublishedContent img)
{
<img class="hero__bg" src="@img.GetCropUrl(1920)" alt="" role="presentation">
}
</section>
Partial Views and Child Actions
@* Views/Partials/_Navigation.cshtml *@
@using Umbraco.Cms.Web.Common.UmbracoContext
@inject IUmbracoContextAccessor UmbracoContextAccessor
@{
var umbracoContext = UmbracoContextAccessor.GetRequiredUmbracoContext();
var root = umbracoContext.Content?.GetAtRoot().FirstOrDefault();
var nav = root?.Children.Where(n => n.Value<bool>("showInNav"));
}
<nav class="main-nav">
@foreach (var item in nav ?? [])
{
<a href="@item.Url()"
class="@(item.IsAncestorOrSelf(umbracoContext.PublishedRequest?.PublishedContent) ? "active" : "")">
@item.Name
</a>
}
</nav>
Image Cropper
Umbraco uses ImageSharp. Crop configured in media type:
@* With named crop *@
<img src="@image.GetCropUrl("listCard")" alt="@image.Name">
@* With parameters *@
<img src="@image.GetCropUrl(width: 400, height: 300, imageCropMode: ImageCropMode.Crop)" alt="">
@* WebP with fallback *@
<picture>
<source srcset="@image.GetCropUrl(800, imageFormat: ImageFormat.WebP)" type="image/webp">
<img src="@image.GetCropUrl(800)" alt="@image.Name">
</picture>
Development Timelines
Template set for corporate site (Layout, navigation, 6–8 page types, block editor): 2–3 weeks. Single template with custom controller and View Components: 3–5 days.







