When building a content-driven website, there are two goals that are often in tension: give visitors a fast, modern, SEO-friendly site, and give the content team a CMS they can actually use without calling a developer every time a page needs to change.
Our answer is to split the problem in two and let each half of the stack do what it's best at:
- Umbraco 17, running on .NET 10, as the content backend
- Next.js 16 with React 19, running on the App Router, as the presentation layer
Here's why we made that call, and how it plays out in practice.
Two apps, one clean contract
Rather than bolting a headless CMS onto an existing site, we build the two halves as genuinely independent applications that only talk to each other over HTTP:
- The Umbraco installation is the backoffice, the content types, the media library, the database. This is where content editors log in, build pages out of reusable blocks, and manage content like blog posts.
- The Next.js app is a standalone frontend. It never touches the Umbraco database directly, every piece of content it renders comes from an HTTP call.
That separation is the whole point of a "headless" CMS: the people managing content and the people managing the website's code can move independently. CMS upgrades don't require a frontend deploy, and frontend releases don't require touching the CMS.
The content pipeline: Umbraco's Delivery API
Umbraco 17 ships with a Content Delivery API a built-in, versioned JSON API that exposes published content without any custom plumbing. We lean on it heavily rather than building a bespoke API from scratch, which saves real development time.
A page fetch from Next.js looks like this in practice:
GET /umbraco/delivery/api/v2/content/item/home?expand=properties[$all[properties[$all[properties[$all]]]]]
That expand parameter is doing a lot of work: pages are built by editors dragging and dropping content blocks (hero sections, feature grids, image-and-text panels, calls to action) using Umbraco's Block Grid editor, and those blocks can be nested. The expansion query lets us pull an entire page blocks inside blocks inside blocks in a single round trip instead of chasing references with follow-up requests.
On the Next.js side, a small typed fetch helper wraps every one of these calls, attaches an API key, and lets Next's own data cache handle short-lived revalidation. There's no GraphQL client, no heavyweight SDK just fetch(), TypeScript types that mirror the Umbraco content model, and a short cache window so published changes show up almost immediately without hammering the CMS on every request.
Rendering: server-rendered, block by block
Generic content pages funnel through a single catch-all route that renders server-side on each request. Instead of one big template, rendering is driven by content type: a dispatcher component looks at what kind of block came back from Umbraco a feature section, an image gallery, a testimonial, a two-column layout and hands it off to the matching React component.
The effect is that adding a new content block is mostly a two-sided change: define the element type in Umbraco, add a matching case in the renderer. Editors get a new building block to compose pages with; developers don't have to build new pages from scratch every time a new layout is needed.
Beyond the CMS defaults
The Delivery API covers page content well, but a few features need more than "fetch a page and render it":
- Filtering and search. Category pages, tag pages, and "previous/next" navigation are backed by custom API endpoints that walk the content tree directly, and free-text search runs on Umbraco's built-in Lucene-based indexer rather than an external search service. That avoids standing up and paying for a separate search product just to let visitors search content.
- Forms. Submissions are validated in the browser, sent through a Next.js API route, and stored by a small custom endpoint on the Umbraco side with a lightweight dashboard added to the Umbraco backoffice so the team can review submissions without leaving the CMS they already use every day.
- Media handling. Images uploaded into Umbraco's media library are served through Next.js's image optimizer, so editors don't need to think about responsive image sizes or formats that's handled automatically on the way out.
What this stack gets you
Putting it together, the architecture gives:
- Speed, from server rendering and Next.js's image and asset optimization
- SEO control that lives with content, since page titles, descriptions, and indexing rules are authored directly in Umbraco and flow straight into page metadata no separate SEO tooling to keep in sync
- A content team that isn't blocked by developers, because new pages are built from existing blocks rather than new code
- A frontend that isn't blocked by the CMS release cycle, because the two halves deploy independently
Trade-offs worth naming
Being candid, this approach isn't free. Running on Umbraco 17 and Next.js 16 means running close to the front of both ecosystems fewer battle-tested examples to lean on, but a stack that stays current for years rather than needing a rewrite in one. And a headless setup means CMS preview/draft workflows, sitemaps, and secrets/hosting management all need to be built or configured deliberately they don't come for free just because the CMS has them out of the box in a traditional, non-headless setup.
For a site with a real content operation behind it, splitting "where content lives" from "how it's presented" is worth the extra moving part. It lets each side of the team content and engineering work at their own pace, without waiting on each other.
Top comments (0)