MIGRATION · April 14, 2026 · 11 min read
Migrating SharePoint Site Pages: why canvas layout matters
The first time someone migrates a SharePoint site with a tool that treats pages as list items, they get a surprise on Monday morning. The page is there. The URL resolves. The text shows up. But the layout is flat, the image web parts are gone, the Highlighted Content rollups are empty, and the custom SPFx hero web part renders as a grey error box. The migration technically succeeded. The page is technically broken.
This post explains why modern SharePoint pages break under naive migration, what the Graph pages API actually returns, and how MigrationFox’s SharePoint Site Migration preserves canvas layout and web-part configuration across tenants.
What a modern SharePoint page actually is
In classic SharePoint, a page was basically an HTML document stored in the Pages library. You could almost get away with reading the .aspx file, copying the body, and pasting it into a new file on the destination. It was ugly, but it roughly worked.
Modern pages — everything created since the SitePages content type replaced the classic Wiki Page model — are not HTML documents. They are structured canvas layouts backed by a JSON schema. A page has:
- Sections, each with a column layout (one-column, two-column, three-column, vertical section)
- Columns inside each section, each with a factor that controls relative width
- Web parts inside each column, each with a strongly-typed ID and a JSON property bag
- Page-level metadata (title, banner image, banner layout, description, promoted state)
When you open a page in the browser, SharePoint deserialises that structure, looks up each web part by its type ID, and renders it. The HTML you see in devtools is the output of rendering, not the source of truth. The source of truth is the canvas JSON.
Why list-item migration breaks pages
A lot of generic migration tools treat the Site Pages library the same way they treat any document library: enumerate items, copy files, done. For document library files that approach is fine — a PDF is a PDF and its bytes are self-contained.
For a Site Pages library it fails in several ways at once:
- The canvas JSON lives in a hidden column (
CanvasContent1) that the tool may not carry across correctly because it exceeds the list-item field-length thresholds SharePoint imposes - Even if the canvas JSON is copied byte-for-byte, web-part references to lists, libraries, or images include the source site’s URLs and IDs — which do not exist on the destination
- Third-party SPFx web parts reference package IDs that may not be installed on the destination tenant; the web part loads as an error tile
- Page-level properties like promoted state (“Make this a news post”) are not part of the canvas JSON; they are set through a separate publishing API and have to be replayed explicitly
What the user sees: a list of files in the destination Site Pages library that look right in the file listing but render as rubble when opened. And because list-item migration “succeeded”, there is nothing in the migration log pointing at the problem.
The right way: Graph Pages API
Microsoft exposes the canvas model through the Graph pages endpoint under /sites/{site-id}/pages. A GET returns the full page object, with the canvas structure as a typed JSON tree rather than an opaque HTML blob. For each web part it returns:
- The web-part type ID (a GUID that identifies which web part it is)
- The instance ID (a unique per-page GUID so the page can reference it internally)
- The properties JSON — all the configuration the user has set via the property pane
- The server-processed content (for text parts, this is the rich text; for image parts, the image reference)
- The column the web part sits in, and its order within that column
A POST to /sites/{site-id}/pages with the same structure creates a new page on the destination, with the same canvas layout, the same web parts, and the same properties. Followed by POST to .../publish and optionally .../promoteToNewsPost, and the page is live.
The Graph Pages API is the right abstraction for modern pages in the same way that contentTypes is the right abstraction for schema. Treating pages as list items is treating structured data as if it were a string.
How MigrationFox walks a page
For each page in the source Site Pages library, we do something close to this:
1. GET /sites/{source-site}/pages/{page-id}
2. Read the canvas sections, columns, and web parts
3. Walk each web part:
a. Copy text/image/link web parts as-is
b. Rewrite list-viewer web parts to point at the new list ID
on the destination (we know this because we migrated
the list earlier in the phase ordering)
c. Flag SPFx web parts whose package ID is not installed
on the destination for human review
4. POST the reconstructed page object to /sites/{dest-site}/pages
5. POST /publish to make it live
6. If the source was a promoted news post, POST /promoteToNewsPost
The important bit is step 3b. Web parts reference data by ID, not by name. If a Highlighted Content web part was rolling up “the last five documents from the Policies library”, the reference in the canvas is the GUID of the source Policies library, not the word “Policies”. For the web part to work on the destination, that GUID has to be rewritten to the destination library’s GUID — which means the list has to have been migrated first and its ID captured. That is why schema before items, items before pages is the mandatory phase order.
Cross-tenant URL caveats
Even with perfect canvas preservation, cross-tenant page migration has a class of reference that you cannot transparently fix: explicit hyperlinks in text web parts that point at full source-tenant URLs.
A rich-text web part might contain something like <a href="https://acme.sharepoint.com/sites/hr/handbook/welcome.aspx">Read the handbook</a>. That URL lives inside the rendered HTML of the text web part, not as a typed reference that the migration engine can identify with certainty. The destination tenant is newcompany.sharepoint.com, so the link is now broken.
MigrationFox’s approach is conservative: we rewrite obvious cases (the URL of a migrated site we know about, with a clean path match) and flag the rest in the migration report with the page name, the web-part ID, and the offending URL so a content owner can fix them. We explicitly do not do fuzzy URL rewriting on rich-text bodies because the risk of rewriting something that was a deliberate external link (to the old tenant, or to a SharePoint page someone kept public on purpose) is higher than the benefit of automating the 80% case.
What actually gets preserved
| Element | Preserved | Notes |
|---|---|---|
| Canvas sections & columns | Yes | Full layout fidelity including vertical sections |
| Text web parts | Yes | Rich text, formatting, inline links |
| Image web parts | Yes | Image file re-uploaded into Site Assets on destination |
| Highlighted Content / List viewer | Yes | Source list ID rewritten to destination list ID |
| Quick Links, People, Events | Yes | Internal references rewritten where possible |
| Banner image + layout | Yes | Including the banner’s focal point coordinates |
| Promoted / News post state | Yes | Replayed via promoteToNewsPost |
| Third-party SPFx web parts | Reference only | Package must be installed on destination; surfaced in report |
| Page-level approvals / pending drafts | Partial | Current published version migrates; draft workflow state does not |
| Page permissions (unique) | Yes | Via the cross-tenant UPN mapping |
The SPFx package problem
Custom SPFx web parts are the one category that cannot be fully automated, and it is worth being honest about why.
An SPFx web part is a packaged solution (.sppkg) that has to be uploaded to the destination tenant’s App Catalog and then deployed to the sites that use it. That is a tenant-admin action, and it happens in a system (the App Catalog) that a migration tool cannot write to without escalated privileges. What MigrationFox does instead is enumerate every SPFx web part referenced by any page in the source, look up its package ID and version, and produce a pre-flight list for the tenant admin. The list has:
- Package ID and display name
- Version number
- Which pages use it, and where it appears on each page
- Whether the package is already installed on the destination (if the admin has connected the destination Graph credential)
The admin installs the missing packages before or during migration; the pages migrate with the web-part references intact; everything lights up on first page load. If a package is genuinely gone from the world (the vendor shut down, the internal developer left), the web part renders as an error tile on the destination — the same experience as on the source once the package was removed there — and the migration report flags it for content cleanup.
The migration order, with pages included
Site Pages are the penultimate phase in a clean SharePoint site migration, after items but before permissions:
- Site columns and content types
- Lists and libraries with bound content types
- List items and files
- List views
- Site Pages (including canvas, web parts, publish state)
- Permissions — applied against the cross-tenant UPN mapping
The reason pages come after items is the ID-rewriting requirement we described earlier: a page that references list X needs list X’s destination ID to be known at page-creation time. The reason pages come before permissions is that the permissions replay is the last thing to touch the site and we want all content to exist before we start locking it down.
What we are still improving
A few page-related capabilities are on the roadmap but not shipped. We are calling them out so you can plan around them:
- Page comments and likes. Graph does not currently expose a supported write path for migrating historical page comments. Current comments do not come across.
- Draft versions. Only the current published version of a page is migrated. If a page has an in-progress unpublished draft, that draft is not preserved.
- Page version history. Like list item version history, pages carry a version stack that cannot be replayed through Graph. The current canvas migrates; prior versions do not.
For most migrations, these gaps are acceptable. For records-retention use cases where the page history is itself a regulated artefact, treat SharePoint pages the same way you would treat list items with version-history requirements: capture the history as a sidecar on the source before migration, and keep it queryable.
Related reading
- SharePoint Site Migration platform page — the full capability matrix
- SharePoint migration best practices: 2026 edition — sequencing and audit checklist
- Cross-tenant SharePoint permissions without user-mapping hell — the permissions phase
- How MigrationFox cut SharePoint migration time by 50% — the speed audit
Get started
Run a free pre-flight on your SharePoint site at app.migrationfox.com/register. The dry-run report enumerates every page, every web part, and every SPFx package the destination will need before cutover.