SharePoint Site Migration
Cross-tenant site-collection migration covering content types, list views, version history, and modern Site Pages. Runs in three sequential phases on top of the base document-library copy.
This is not SPMT
MigrationFox does not preserve original authors or Modified/Created timestamps on migrated items. That behaviour is on the roadmap but not shipped. Items land with the service principal as author and the migration run time as their timestamps. If you need full date/author preservation today, run SPMT for the file body and use MigrationFox to carry the site scaffolding (content types, views, pages, permissions) alongside it.
The site connector uses the credentials from the SharePoint / OneDrive setup (Entra ID application with Sites.ReadWrite.All + Files.ReadWrite.All). No extra scopes are needed for Phases 1 and 3. Phase 2 web-part rewriting uses only the same Graph endpoints.
Phase 1Content Types
Before any list or library is created at the destination, the connector enumerates site-level content types at the source and recreates the custom ones at the destination. Parent content types (e.g. Item, Document) are referenced by ID rather than re-created, so the hierarchy lines up.
What gets copied
- Every non-sealed, non-built-in content type in the site-level scope.
- Each content type's
name,description,group, parent content type reference, and linked column list (columnLinks). - Custom site columns referenced by those content types — created first so the content type's
columnLinksresolve on creation.
What does not
- Content-type-level event receivers, document information panel (DIP) templates, and document sets nested metadata.
- Hub-site-published content types — these should be re-published from the destination hub after the migration completes.
Graph endpoint used: GET /sites/{id}/contentTypes?$expand=columnLinks then POST /sites/{id}/contentTypes at the destination.
Phase 2List Views
After each list is created and items are in place, the connector copies the custom views for that list. View creation is best-effort via the Graph Pages API — enough for the common view shapes (filter on one or two columns, a sort, a column set, a row limit) but not a full CAML round-trip.
What works
- View title, row limit, and visible column set.
- Default view designation (exactly one view marked default per list).
- Simple filters and sorts expressible in Graph's view object.
What does not round-trip cleanly
- Complex CAML. Views hand-authored with nested
<Or>/<And>groups, calculated-column filters, or membership-based filters may land with a simplified filter or no filter at all. The view is still created — you just need to re-open it in the browser and refine the filter. - Grouped views with metadata-navigation headers. The grouping is preserved; the metadata nav is lost.
- Calendar, Gantt, and Board (Kanban) views. These are recreated as standard views with the column set preserved — you'll need to re-pick the layout type in the destination list's view settings.
Audit views post-migration
The migration report flags every view where the destination Graph response did not include the full source filter text. For lists where views matter (e.g. an HR onboarding tracker), open the flagged views in the destination and verify the filter before handing the site off to users.
Phase 3Version History
Graph does not expose a way to write historical versions of a list item with their original authors or timestamps. To avoid silently dropping that history, MigrationFox materializes it as JSON snapshots in a hidden long-text column on each item.
How it works
- On each destination list that has any versioned item in the source, the connector provisions a hidden column called
_VersionHistoryof typeNote(long text, plain — no rich text). The column is hidden from views but remains query-able via the item's field collection. - For every item with more than one version at the source, the connector pulls all historical versions via
GET /sites/{id}/lists/{listId}/items/{itemId}/versions. - The versions are serialized as a JSON array — each element containing the version label, the modifier's UPN at the source, the source timestamp, and the full field-value map at that revision — and written into
_VersionHistoryon the destination item.
Reading the history back
The hidden column is readable with any standard SharePoint client. A representative PowerShell snippet:
Connect-PnPOnline -Url "https://contoso.sharepoint.com/sites/hr" -Interactive $item = Get-PnPListItem -List "Employees" -Id 42 $history = $item["_VersionHistory"] | ConvertFrom-Json $history | Format-Table versionLabel, modifiedBy, modifiedAt
If you later add a proper history-preserving API to the destination (Microsoft does not currently have one), the JSON payload is rich enough to replay every version in order.
Long-text column limits
A SharePoint Note column allows up to ~63,999 characters. Items with extremely deep version histories (thousands of revisions on a wide row) can overflow; in that case the connector writes the most recent 500 versions and logs a warning on the item. Overflow is rare outside of automation-driven lists.
Site Pages (Modern)
Modern Site Pages — the canvas/web-part page model that replaced wiki pages — do not round-trip through the generic list-item migration because their content lives in canvasLayout plus web-part JSON rather than simple field values. They run through a separate Graph Pages API path.
What copies
- Page title, name, description, promotion state (news post vs. regular page), and page layout.
- The full
canvasLayout: sections, columns, and their ordering. - Every web part's type ID, instance ID, and serialized properties (the
webPartDatablob). - Best-effort publish after creation —
POST /sites/{id}/pages/{pageId}/microsoft.graph.sitePage/publish.
Manual fix needed: cross-tenant web-part references
Many web parts embed absolute URLs pointing back at the source tenant — a Highlighted Content web part that queries "files in contoso.sharepoint.com/sites/marketing", a News web part pinned to a source-tenant news site, a YouTube/Stream embed that references an internal Stream URL. The connector carries the exact JSON across, so those web parts will still render against the source tenant until they are repointed. This is always a manual step. The migration report lists every page that contains a web-part property matching the source host name so you can review them in order.
Recommended workflow
Migrate documents and lists first so the destination has real content. Run Site Pages last. After the run, open each flagged page in edit mode, repoint the Highlighted Content / News / Stream web parts to the destination equivalents, and republish. The pages already look correct structurally at that point — you are only fixing the query targets.
Permissions & Cross-Tenant UPN Mapping
On a same-tenant migration, permissions copy as-is — the source principals resolve against the destination directory because it is the same directory. On a cross-tenant migration (the common case for mergers and divestitures) users have different UPNs on the two sides and you need a mapping.
CSV format
The permission-mapping CSV uses the same two-column shape as the bulk mail migration CSV:
sourceEmail,destEmail jane.doe@source-tenant.com,jane.doe@dest-tenant.com raj.patel@source-tenant.com,raj.patel@dest-tenant.com ext-vendor@gmail.com,vendor.contractor@dest-tenant.com
Rules carry over identically: optional header, both addresses validated, one mapping per row, no quoting. Addresses not present in the CSV are dropped from the permission set on the destination — the migration report lists every source principal that had no mapping so you can decide whether to re-run with a richer CSV or leave them removed.
What gets mapped
- Direct user grants on the site, libraries, lists, folders, and items.
- Group memberships — if the group name matches between tenants, group-level permissions are applied to the destination group; otherwise the members are mapped individually via the CSV.
- Sharing links on files — the link is re-created at the destination with the mapped user set.
What does not
- Azure AD / Entra security groups that exist only in the source tenant — they have no destination equivalent. Their members are added directly per the CSV; the group itself is not created.
- External / guest principals that are not mapped in the CSV. By policy the migration does not carry unmapped externals to the destination.
Running a Site Migration
- In your project, add SharePoint as both source and destination with an app-only credential on each side. See SharePoint / OneDrive setup.
- Create a new job with Site migration as the job type. Pick source site URL and destination site URL (both must exist — the connector does not create the destination site collection).
- Upload the permission-mapping CSV if this is a cross-tenant migration.
- The phases run automatically in order: content types → lists with items → list views → version history snapshots → Site Pages → permissions. You can watch each phase tick off in the dashboard.
Related
- SharePoint / OneDrive setup — credentials shared with this migration
- Restructuring Wizard — use before a site migration when the source is a mega-site
- Bulk mail migration — same UPN CSV format