Tutorials
Short, standalone tutorials covering specific problems we've hit on the Umbraco Community site and the approaches we took to solve them.
Each tutorial is self-contained: it states the concrete problem, explains the trade-offs of our solution, and walks through the implementation file by file. They're written to be picked up on a different Umbraco project (or no Umbraco project at all) — but they reference real code in this repo, so anyone working on the site can jump straight to the source if they prefer to read code over prose.
These sit alongside the other docs in this folder rather than replacing them:
- Primers orient you to a whole area of the codebase (e.g. the frontend) with links out to the deeper docs for each topic. Start here if you're new.
- How-to guides like
BUILDING_PAGES.mdandBUILDING_BLOCKS.mdtell you the conventions for adding new things to this codebase. - Operational notes like
LESSONS_LEARNED.mdcover workflow gotchas (Umbraco upgrades, cloud deploys, schema management). - Tutorials (this folder) explain why a particular piece of the codebase is shaped the way it is, and how to build something similar from scratch.
How to read these
Tutorials are split into two kinds:
foundations/— pieces of code that other tutorials build on. Read these first if a refinement says it's a prerequisite.refinements/— extensions, bug fixes, or improvements layered onto a foundation. Each refinement names the foundation it depends on at the top.
You don't need to read the suite in order. Each tutorial points at the next logical stop in its closing section, so you can either follow a thread the whole way through, or just jump straight to whichever problem happens to match the one in front of you.
What's here
Foundations
- Building an inline SVG TagHelper for Umbraco — A custom
<svg-src>TagHelper that reads SVG files from Umbraco media and inlines them into the page, so they can be styled and animated with CSS. Adapted from Warren Buckley's Our.Umbraco.TagHelpers. - Resolving content in a multi-tenant Umbraco site — One Umbraco instance, multiple tenant root content nodes. Walks through the
Root()+GetSiteSettings()pattern that keeps every content lookup scoped to the current request's tenant, the document-tree shape it assumes, and the small set of helpers that consumers (menu, footer, SEO, sitemap) lean on. - Wiring Vite's manifest into Umbraco's Razor pipeline — A pair of TagHelpers (
<script vite-src>/<link vite-href>) that point at the Vite dev server on:5123for HMR in development and at the content-hashed files named inmanifest.jsonin production. Covers the entry-name convention, dev-vs-prod CSS handling, andIFileProvider.Watch-based cache invalidation that survives a deploy without an app restart. - Progressive enhancement of async-rendered DOM with MutationObserver — Enhancing DOM a third-party widget renders on its own schedule. The
<dc-form-steps>element waits for an Umbraco Forms form (async-rendered inside<umb-forms-render>) to appear, turns it into a multi-step form once it does, and disconnects the observer cleanly. The pattern transfers anywhere your enhancement might run before the thing it enhances exists. - One master block data type, restricted per consumer — The content-modelling decision behind Block Restrictions: keep a single Block Grid/List data type holding every block, point all document types at it, and narrow the menu per document type with a rule — instead of a near-duplicate data type per document type (and per tenant). Frames the data-type-sprawl problem, the master-plus-rule alternative, and links to the resolution and editor-enforcement tutorials that implement it.
- Configuration that inherits down the content tree — The resolution engine behind Block Restrictions, framed generally: attach a rule to a document type, resolve it for any node by walking content ancestors (not the type hierarchy), let the closest rule win, and fail open when nothing matches. Covers the two-tier cache and the generation-counter trick for invalidating an unknowable set of cached reads on a single write. Transfers to permissions, feature flags, and theming that cascade down the tree.
Refinements
- Scoping inline SVG
<style>to prevent class-name bleed (builds on the inline SVG TagHelper) — Illustrator-exported SVGs ship<style>blocks with generic class names (.st0,.st1, …) that are document-scoped, not SVG-scoped. Two such SVGs on one page fight over the same class names. Fix: have the TagHelper add a deterministic class per SVG file and prefix every internal selector with it. - Caching the scoped SVG output (builds on the scoping refinement) — Once the scoped SVG markup is deterministic per media path, every render of the same SVG is byte-identical. Wrap the read + sanitise + parse + scope work in Umbraco's
RuntimeCachekeyed by media path; serve the cached blob directly on the hot path. Skips the cloud media round-trip entirely after warm-up. - Per-tenant 404 pages with a custom
INotFoundPageResolver(builds on multi-tenant content resolution) — When the request 404'd there's no current page to anchor tenant lookups off. Resolve the tenant root from the domain binding instead (request.Domain?.ContentId), walk descendants for aPageNotFoundcontent type, and return it for theUmbraco.Community.NotFoundTrackerpackage to serve with HTTP status 404. - Tenant-aware fallback for schema and SEO metadata (builds on multi-tenant content resolution) — Every page emits
Organizationschema, but editors forget to fill in tenant brand fields. A smallOrganizationSchemaBuilderaccepts a nullableSocialSettingsand falls back to hardcoded constants so unconfigured tenants still produce valid schema. - Syncing custom backoffice configuration across environments — Block-restriction rules live in a custom database table, so they don't ride Umbraco's deploy pipeline and aren't version-controlled. The fix: mirror every save to a git-committed JSON file (DB → disk, automatic and one-way), pull back via a reviewed diff on a dashboard (disk → DB, manual — because import deletes orphaned rules), and a zip path for Cloud where you can't touch the filesystem. The transferable lesson for any package that owns config: automatic out, deliberate in.
- Wrapping Umbraco's native block editor with restriction filtering — A custom property editor UI that reuses the native Block Grid/List schema but wraps the native element, filtering its allowed-block list down to a tree-resolved restriction. Covers Light-DOM context propagation, the recreate-on-restriction pattern when the rule arrives late, the Grid-vs-List filtering asymmetry, and the clipboard value translators that copy/paste needs to survive the wrapping. Compares the wrap approach against the server-side modal-replacement approach of Kraftvaerk.Umbraco.BlockFilter.
Planned
The backlog in IDEAS.md lists tutorials that haven't been written yet. Each idea there also has a placeholder file under foundations/ or refinements/ with a status callout and a "what this will cover" sketch — useful if you're picking one up to write, or just want to scan what's coming without reading the backlog index.
Contributing a new tutorial
When adding a new tutorial:
- Check the backlog first. If your topic is already there, it has a placeholder file under
foundations/orrefinements/— expand the stub in place rather than creating a duplicate. If your topic isn't listed yet, that's fine; just pick the right folder for it (foundations stand alone; refinements depend on an earlier piece of code) and create a new file with a kebab-case filename that describes the technique rather than the bug. - Follow the section structure used by the existing tutorials: a one-paragraph framing, then The problem → Why the obvious fix doesn't work → Our approach → Walkthrough → Alternatives we considered → Trade-offs and known limits. For foundation pieces, swap "The problem" for Why you might want this and "Why the obvious fix doesn't work" for What we're building.
- Link to real files in this repo using paths relative to the tutorial file (e.g.
../../../src/UmbracoCommunity.Web/TagHelpers/SvgTagHelper.cs). - Credit prior art. If the code is adapted from a community project, lead with a "Credit where it's due" section linking the source and naming contributors.
- Move the tutorial out of "planned" and into the relevant section above: add it to What's here, and update its backlog entry in
IDEAS.md(either strike it through with a "shipped as ..." note, or remove the bullet entirely) — all in the same commit.
You don't need to maintain a contributors list by hand. Each rendered doc shows a Contributors section generated from git history (docs/contributors.generated.json, produced by npm run generate:doc-contributors and refreshed in CI). Open a PR and you'll be credited automatically — with your GitHub avatar where your commit email is linked to your account.
Foundations
-
Secured backoffice Management API endpoints
Community content on the new Umbraco backoffice almost exclusively covers the UI side — property editors, dashboards, workspace views. The C# endpoints those UIs actually call are consistently under-documented. The BlockRestrictionApiController in this repo is a clean example of the secured backend half: routing under /umbraco/.../api/v1, locking endpoints down with [Authorize(Policy = AuthorizationPolicies.SectionAccessContent)], Swagger doc registration so the endpoints show up in the API docs, and a typed fetch wrapper on the client that pulls the user's backoffice bearer token automatically.
-
Configuration that inherits down the content tree
A common need in a CMS: attach a piece of configuration to something high up, and have everything below it pick the value up automatically unless it's overridden closer to home. Block Restrictions does it with allowed-block rules — set a rule once, and every page beneath inherits it — but the shape is general. This tutorial is the resolution engine behind that: how to attach a rule, resolve it for any node by walking up the tree, cache the answer so you're not re-walking on every request, and fail open when nothing is configured. It's the "how rules resolve" half of the Block Restrictions trio; the editor-wrapping refinement is what consumes the answer.
-
Building a touch-friendly drag-to-scroll slider in vanilla web components
Carousel libraries like Swiper and Embla are great until you measure the bundle cost. This tutorial will walk through the <dc-slider> component in this repo — a small Lit web component that supports touch drag (follows the finger and snaps to the nearest slide on release), desktop hover-zone navigation, and explicit arrow buttons as an opt-in. The aim is to show what a usable scroller looks like when it's built from scratch with native APIs rather than pulled in as a dependency.
-
Building an inline SVG TagHelper for Umbraco
SVGs are wonderful for icons and brand marks — small, sharp, infinitely scalable — but only if you can actually style them from CSS, and that's where things tend to get fiddly. This tutorial walks through the <svg-src> TagHelper that the Umbraco Community site uses to inline SVG files from Umbraco media straight into the rendered Razor view, where CSS and JavaScript can then reach inside them. It's a foundation piece — most of the SVG-related tutorials in this suite build on top of it.
-
Polite animation with IntersectionObserver, requestAnimationFrame, and reduced-motion
The <dc-image-slider> component auto-scrolls a row of images — but you don't want it to keep burning a CPU core when the user can't see it, or when they've asked the OS to dial down motion. This tutorial will walk through the small composition that makes the animation cheap and polite: requestAnimationFrame for the loop itself, IntersectionObserver to pause when the slider scrolls off-screen, visibilitychange for tab switches, and prefers-reduced-motion for accessibility. The general lesson is how to animate something well without reaching for a library.
-
Resolving content in a multi-tenant Umbraco site
Running multiple sites from a single Umbraco instance is one of those things that looks easy until you've shipped one — and then suddenly every content lookup needs to know which tenant it belongs to. This tutorial walks through the pattern the Umbraco Community site uses for multi-tenancy (several distinct sites out of one Umbraco instance) and the small set of helpers that keep every content lookup scoped to this request's tenant rather than wandering off into another one's tree.
-
Progressive enhancement of async-rendered DOM with MutationObserver
Progressive enhancement has a tidy mental model: the server sends working HTML, and your JavaScript layers extra behaviour on top once it loads. That model quietly assumes the HTML you want to enhance is there when your code runs. But what happens when it isn't yet — when the very thing you mean to enhance is rendered, asynchronously, by some other component that owns its own timing? Your enhancement runs, finds nothing to do, and gives up. This tutorial walks through the small MutationObserver pattern the Umbraco Community site uses to solve exactly that: the <dc-form-steps> element waits for an Umbraco Forms form to finish rendering, turns it into a multi-step form once it appears, and then disconnects cleanly.
-
A nonce-based Content Security Policy in ASP.NET Core Razor
A strict Content Security Policy that bans inline scripts will catch most XSS vectors at the browser level — and it will also make life impossible for every legitimate inline script your views happen to emit. The standard answer is to nonce every inline script and stamp the same nonce into the CSP header, so the browser allows scripts whose nonce matches and blocks everything else. This tutorial will walk through the NonceTagHelper + Joonasw integration this site uses, plus a per-request escape hatch for the rare endpoint that needs CSP disabled entirely. CSP-in-.NET is poorly documented; the aim is for this to be the post one of us wishes had existed.
-
One master block data type, restricted per consumer
This is the content-modelling decision that the whole Block Restrictions package exists to serve, written down on its own because it's the why behind two other tutorials and it's easy to lose under their mechanics. The short version: keep a single Block Grid (or Block List) data type that holds every block the site could ever use, point every document type at that one data type, and then narrow the offered set per consumer — per document type, and by inheritance the content nodes beneath it — with a restriction rule, rather than building a separate data type for every document type that wants a different subset of blocks.
-
Generating utility classes from design tokens with a custom PostCSS mixin
Utility-first CSS frameworks like Tailwind give you the ergonomics of .pt-md and .mx-xs — but they want to own your design system. The rhythm mixin in this repo flips that arrangement: you write your spacing tokens in root.css as CSS custom properties, hand them to a small postcss-mixins rule, and you get the same utility classes generated for you — driven entirely by your own tokens, with no opinion from a framework. This tutorial will walk through writing the mixin, the modifier suffixes it emits, and how to layer it on an existing PostCSS pipeline.
-
Site search backed by Umbraco's Examine ExternalIndex
"How do I add search to my Umbraco site?" is a perennial community question, and most answers stop at the single-tenant happy path. This tutorial will walk through the recently-added search on the Umbraco Community site: a SearchPage doc type, a typed SearchService that queries Umbraco's Examine ExternalIndex, a render controller that paginates the results, and the multi-tenant twist — only return hits under the current tenant's content root, so a search on Site A doesn't leak Site B results.
-
Wiring Vite's manifest into Umbraco's Razor pipeline
Vite bundles your frontend and Umbraco renders your views, and sitting between the two is a single <script> tag that needs to behave completely differently depending on where it's running. Locally, you want the browser talking to the Vite dev server so you get hot module replacement (HMR — your edits to JS and CSS show up in the browser without a full page reload). In production, you want it loading a content-hashed file whose name changes with every build. Same tag, two different use cases. This tutorial walks through the pair of TagHelpers the Umbraco Community site uses to bridge them — <script vite-src> and <link vite-href> — so that you can write one stable line of Razor and let the C# work out, per environment, whether to point at localhost:5123 for HMR or at the hashed asset named in Vite's manifest.json.
Refinements
-
Caching the scoped SVG output
Once the SVG TagHelper produces deterministic scoped output, every render of the same media item is byte-identical — which is a rather lovely property to have, because it makes the whole pipeline cacheable. We can do the read + sanitise + parse + selector-prefix work once and then happily reuse the result for an hour. On cloud-hosted media in particular this is the difference between every page render making N media-storage round-trips and making zero of them, which is a meaningful difference in latency for the end user.
-
Output cache policies for slow upstream APIs
When your site's performance is held hostage by a slow or rate-limited third-party API (Sessionize, GitHub, anything off-prem), aggressive caching is usually the answer — and ASP.NET Core's [OutputCache] with named policies is the right tool for it. This refinement will walk through the OutputCachePolicies class in this repo, when [OutputCache] beats ResponseCaching (it survives across instances, you control the key, you can vary by query), what cache-key shapes make sense for tenant-scoped data, and how to fail gracefully when the upstream is rate-limited or 500s.
-
Per-tenant 404 pages with a custom INotFoundPageResolver
Multi-tenant Umbraco sites need multi-tenant 404 pages — it stands to reason that if a request lands on Tenant A's domain and ends up at a missing URL, the user shouldn't suddenly be looking at Tenant B's header and footer just because we couldn't find the page they asked for. This tutorial walks through the small resolver that makes that work, and the subtlety that forces it to resolve the tenant from the domain binding rather than from the (non-existent) current page.
-
Scoping inline SVG <style> to prevent class-name bleed
There's a surprisingly satisfying class of "the logo went the wrong colour" bugs that all trace back to the same root cause: inline <style> blocks inside SVGs aren't actually scoped to the SVG they live in, even though every bit of your experience tells you they ought to be. This tutorial walks through how we ran into the problem on the Umbraco Community site, why the obvious fixes don't quite hold up, and how we ended up solving it with a small change to a single TagHelper.
-
Syncing custom backoffice configuration across environments
You've built a feature whose configuration lives in your own table — for us, block-restriction rules keyed by document type. It works beautifully on your machine. Then someone asks the obvious question: a rule you set up locally needs to be on staging and production too, and ideally it should be reviewable in a pull request like the rest of the codebase. Suddenly "it's in the database" is a problem, not a feature. This tutorial is how Block Restrictions makes its configuration travel between environments and live in git.
-
Tenant-aware fallback for schema and SEO metadata
Structured data — the small blocks of Schema.org JSON-LD that crawlers like Google and Bing look for in your page head — needs a publisher: an Organization with a name, a URL, and a logo, that they can attribute the content to. On a multi-tenant Umbraco site, that publisher is naturally per tenant — Site A's publisher is the Umbraco Community brand, Site B's is the events microsite, and so on. The challenge is that tenant brand metadata is editor-configurable, which means (let us be honest with each other here) that it's also editor-forgettable. This tutorial walks through the small pattern that produces valid Organization schema whether the tenant's brand fields are filled in, partially filled in, or entirely absent.
-
Wrapping Umbraco's native block editor with restriction filtering
The Block List and Block Grid editors are two of the most useful things Umbraco ships, and by default they offer every block a data type is configured with to every editor, on every node. We wanted to narrow that list per document type (with inheritance down the content tree) — a "Blog Post" should only offer a handful of blocks, a "Landing Page" the full set. This tutorial is about the backoffice side of that: how to enforce the restriction in the editing UI without reimplementing the block editor, by wrapping the native one instead of replacing it. The sting in the tail is copy-and-paste, which breaks in a genuinely puzzling way once you wrap, and needs a little boilerplate to put right.