Issues

From Umbraco 8 to Umbraco 17 in One Leap: uSync, Cursor, and the Umbraco MCP

Upgrading a CMS across nine major versions sounds daunting — but it's absolutely doable when you have the right tools and a clear process. This guide walks through how we migrated a large Umbraco 8 site straight to Umbraco 17, preserving everything: schema, dictionary, media, and content — without touching a single intermediate version.

The goal was editorial fidelity: keep what the v8 site already expressed, but rebuild it using native v17 property editors and serializers.

The Toolset

Three tools did the heavy lifting:

Tool Role
Cursor Reads the legacy uSync export, understands types and cross-references, orchestrates scripts and API calls.
Umbraco MCP Talks to the v17 Management API to create real database structures before bulk import; provides property editor templates to eep JSON shapes correct
uSync Serializes the result for Git, diffs changes across environments, and bulk-imports large content and media trees

 

The workflow forms a closed loop:

Infer the model from v8 → Materialize it in v17 (MCP) → Validate identifiers → Fix and re-run → Repeat until CMS, disk, and import logs agree.

Why Skip the Intermediate Versions?

Serial upgrades (8 → 9 → 10 → … → 17) are expensive and time-consuming. You can jump straight to v17 when:

  • The canonical site description already lives in uSync under source control
  • You can transform exported XML and JSON to v17 contracts rather than re-clicking everything in the backoffice
  • You treat application code (controllers, composers, views) as a separate track from CMS schema and content

We treated the v8 uSync snapshot as an immutable specification. The v17 model was built through MCP-driven changes, then kept in sync with serialized uSync — making reviews and deployments deterministic and repeatable.

The Core Migration Process

Step 1 — Cursor Reads v8 uSync and Builds the Schema Graph

Cursor doesn't just list files — it builds a dependency graph of the entire v8 export, covering document types, element types, data types, dictionary entries, languages, media types, and content.

From that graph, it derives:

  • Which property editors are in use and how often
  • How v8-only shapes like Nested Content, tabs, list views, and compositions map to v17 equivalents (Block List, Block Grid, modern pickers)
  • Cross-references: property → data type, ncAlias → element type, and any JSON config embedding type keys or picker roots

This graph becomes the contract for the migration — it implements what v8 already declared, upgraded to v17 standards, without inventing a new architecture along the way.

Step 2 — Umbraco MCP Creates v17 Backoffice Structures

Using the schema graph, Cursor calls Umbraco MCP against a live v17 instance to create and update:

  • Element types for blocks
  • Data types with correct editor alias and UI alias
  • Document types with properties pointing at those definitions
  • Block List / Block Grid configurations referencing the correct element type keys

MCP gives immediate feedback on a real database — saves succeed or fail right away, and create responses return the GUIDs that anchor the rest of the validation.

Fallback: Where MCP can't complete a call (e.g. strict server-side schema validation), the same shapes go in via uSync or manual correction. The target contract stays v17 regardless of delivery channel.

Step 3 — Validate System ID Mappings End to End

Umbraco is a web of GUIDs. When editors change — Nested Content → Block List, TinyMCE → Rich Text, legacy pickers → MediaPicker3 — stale references break import and property deserialization.

Validation covers every surface:

Surface What's Checked
Document & element types Each property points at a live data type; block/grid definitions reference element type keys from MCP or stable v8 keys
Data types Config JSON references correct content type keys; editor and UI aliases match v17
Content Block List contentTypeKey, layout, expose (culture/segment); Multi Node Tree Picker and Multi URL Picker shapes; Meganav trees flattened to blocks; Table Generator output as rich text HTML
Media File/image value JSON matches v17 editors; node keys and parent links stay consistent

 

Validation compares what v8 implied, what now exists after MCP, and what serialized content still references. Any mismatch is a blocking defect until fixed.

Step 4 — Re-update Until the Graph Is Consistent

When validation fails — wrong block key, stale data type config, Meganav tree where Block List JSON is expected, a single link object where an array is required — fix it in a short cycle:

  1. Adjust definitions in the backoffice (via MCP or UI), or regenerate v17 uSync so disk matches actual database keys
  2. Re-run content and media migrators so payloads track the final key graph
  3. Re-validate and repeat until representative pages (home, article body, navigation, heavy pickers) import cleanly and edit without serializer errors

This iteration is what makes the jump auditable: backoffice state, Git, and import logs all tell the same story.

How the Layers Fit Together

Concern Primary Tooling
Legacy understanding Cursor reads v8 uSync and builds the schema picture
Live v17 schema Umbraco MCP creates and updates document types, element types, and data types
Identifier integrity Validation across MCP responses, preserved keys, and serialized content
Bulk tree movement uSync import (with optional disk-side transforms for reviewability)
Payload contracts MCP property editor templates aligned with the Management API

Migration Phases

Phase 1 — CMS Settings: Data Types & Document Types

v8 exports use editor aliases and XML shapes that don't match v17's Management API or modern property editor contracts. Cursor's graph drives which MCP operations to run and which v17 editor mapping applies to each property.

Representative editor upgrades:

Umbraco 8 (Legacy) Umbraco 17 (Target)
Umbraco.NestedContent Umbraco.BlockList with element types
Umbraco.TinyMCE Umbraco.RichText (Tiptap-oriented)
Umbraco.Grid Umbraco.BlockGrid
Our.Umbraco.Meganav Block List with navigation element types
Application.Feature.TableGenerator Umbraco.RichText (table JSON → HTML)
Umbraco.ListView Collection-style list UI
Umbraco.MultiNodeTreePicker Same alias with v17 Content Picker UI
Legacy Umbraco.MediaPicker Umbraco.MediaPicker3

 

Ordering matters: Create data types and element types that blocks depend on before the document types that reference them. Freeze schema before heavy content import — otherwise deserialization errors look like bad content when the real issue is identifier drift.

Merging with Existing v17 Types

If the database already had v17 types, we merged rather than replaced: keep hand-tuned definitions, add missing tabs, properties, or data type fields inferred from v8. After every merge, re-run ID validation.

Phase 2 — Dictionary, Languages & Import Rhythm

Languages and dictionary data must exist before multilingual content import. Set them up in the backoffice or via uSync, run settings imports first, then content.

Pro tip: Disable automatic import on every application start after your migration window. Use explicit uSync handler groups (Settings, Content, or full) for one-shot migrations — ambiguous "default" behavior can silently replay a full merge on each deploy.

Phase 3 — Media Types & Media Items

Media types follow the same read → MCP or merge → validate → re-update cycle as document types.

For media items:

  • Update value shapes (e.g. image JSON fields v17 expects)
  • Keep keys and parent relationships aligned with the validated graph
  • Use uSync bulk import for large trees after metadata matches database types
Note: uSync moves structure and metadata, not binary files. Those must exist on disk separately.

Phase 4 — Content: Serialized Values & Identifiers

This is where the most subtle failures hide — in serialized property JSON.

After schema keys are validated, a content pass rewrites payloads to match the v17 document types and data types:

  • Nested Content → Block List
  • Meganav trees → blocks
  • Multi Node Tree Picker / Multi URL Picker → MCP template shapes
  • Table Generator → rich text HTML
  • Recursive fixes inside nested blocks

uSync content import at scale is only reliable when both identifiers and JSON validate against the final model.

Common Pitfalls (and How to Avoid Them)

  1. Host process locking the build output — a debugger or IIS Express holding the site DLL blocks publish or build. Kill the process first.

  2. Duplicate data type keys — two definitions sharing the same GUID prevent a clean merge. Remove or reconcile before proceeding.

  3. Missing application URL at runtime — headless import boots may fail without one. Set Umbraco:CMS:Global URLs explicitly for that run.

  4. Vague import-on-startup defaults — name explicit handler groups (Settings, Content, full) to make one-shot migrations easy to reason about.

  5. Partial content import failures — usually traced to missing cultures or picker JSON still in v8 format. Server logs point at the exact property to fix.

Verification Checklist

Before calling the migration done, run through these checks:

  • [ ] Backoffice — open representative types and data types; confirm tabs, groups, blocks, and pickers behave as editors expect
  • [ ] Identifier graph — no orphaned data type or element keys inside data type JSON, document types, or sampled content
  • [ ] Serialized uSync — configs validate as XML and match the database after export (round-trip check)
  • [ ] Repository search — no stale legacy editor aliases remaining on the v17 surface
  • [ ] Build — solution compiles cleanly after schema changes
  • [ ] Import logs — handler counts, items processed, and no unexpected deserialize errors on critical pages
  • [ ] Templates — after any Umbraco or MCP package upgrade, re-check property editor templates so migrators stay aligned with API changes

Outcome

A direct Umbraco 8 → 17 migration with full editorial fidelity: document types, data types, dictionary, languages, media types, media items, and content nodes — all on supported v17 editors instead of unmaintained v8-era packages.

What made it work wasn't a single magic tool. It was the process:

  1. Cursor read v8 uSync as the specification
  2. Umbraco MCP materialized v17 backoffice structures
  3. Validation caught ID drift across schema and serialized data
  4. Re-updates iterated until CMS, Git, and imports agreed
  5. uSync handled bulk, repeatable movement of the entire tree

The result is a pipeline you can run again after changes: read → create → validate → fix → import → repeat.

Pasang Tamang

Hi, I’m Pasang Tamang, a Microsoft MVP, Tech Lead, and Umbraco specialist based in Kathmandu, Nepal. I currently work at https://www.zaaks.nl/, a Dutch company based in Utrecht, The Netherlands, where I lead .NET and Umbraco development projects. Alongside my Tech Lead role, I have also been involved in managing the ZAAKS! development team and Kathmandu office operations since 2012. I have been working with Umbraco CMS since 2010 and specialize in building modern web solutions using Umbraco and the Microsoft .NET ecosystem.
 
Beyond my professional work, I have been actively involved in the tech community since 2013 as a speaker, event co-host, and community contributor. Since 2023, I have been leading community events focused on Microsoft technologies, driven by my passion for knowledge sharing and developer collaboration. I also regularly write technical articles, primarily on C# Corner, and have developed several Umbraco packages through both company and personal initiatives.
 
I enjoy contributing back to the developer community and have been working closely with Melvin van Rookhuizen since 2012 across professional projects, community initiatives, and the broader Umbraco ecosystem, sharing a common passion for technology and collaboration.
comments powered by Disqus