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:
- Adjust definitions in the backoffice (via MCP or UI), or regenerate v17 uSync so disk matches actual database keys
- Re-run content and media migrators so payloads track the final key graph
- 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)
-
Host process locking the build output — a debugger or IIS Express holding the site DLL blocks publish or build. Kill the process first.
-
Duplicate data type keys — two definitions sharing the same GUID prevent a clean merge. Remove or reconcile before proceeding.
-
Missing application URL at runtime — headless import boots may fail without one. Set Umbraco:CMS:Global URLs explicitly for that run.
-
Vague import-on-startup defaults — name explicit handler groups (Settings, Content, full) to make one-shot migrations easy to reason about.
-
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:
- Cursor read v8 uSync as the specification
- Umbraco MCP materialized v17 backoffice structures
- Validation caught ID drift across schema and serialized data
- Re-updates iterated until CMS, Git, and imports agreed
- 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.