Sanity Migration
SanityOfficialPublished by Sanity
- Skill
- Tool integration
- Software development
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a systematic approach to migrating content from various CMS platforms into Sanity. It treats migration as an ETL (Extract, Transform, Load) project, emphasizing deterministic scripts, content modeling, and validation over manual operations. The skill includes specific guidance for platforms such as AEM, Contentful, Strapi, Webflow, WordPress, Payload, Drupal, and Markdown-based systems, ensuring that content is properly mapped, transformed into Portable Text, and validated before final cutover.
What it does
- Provides a structured workflow for planning and executing CMS-to-Sanity migrations.
- Offers platform-specific guidance for extracting data from common CMS environments.
- Defines best practices for mapping source content to Sanity schemas and Portable Text.
- Requires the creation of migration plans, content inventories, and validation reports.
- Encourages the use of deterministic scripts for repeatable and reliable data imports.
- Includes procedures for handling assets, redirects, and final cutover synchronization.
When to use it
- When replatforming a website or application to use Sanity as the content backend.
- When moving content from legacy systems like AEM, Drupal, or WordPress.
- When migrating static files like Markdown or MDX into a structured CMS.
- When you need to perform a complex data transformation and cleanup during an import.
- When you need to ensure data integrity through validation checks and delta syncs.
Advantages
- Reduces risk by mandating planning and validation steps before execution.
- Promotes repeatable migrations through script-based ETL patterns.
- Ensures content is modeled for Sanity's capabilities rather than just copied.
- Provides clear paths for handling complex data like rich text and assets.
- Supports iterative development through deterministic ID management.
Usage examples
- “I need to migrate my site from Contentful to Sanity. Where do I start?”
- The agent will guide you through the Contentful-specific extraction process, help you plan your content mapping, and suggest using the contentful-to-sanity toolkit for an initial pass.
- “How should I handle AEM content fragments during a migration?”
- The agent will provide instructions on using AEM GraphQL to extract fragments and mapping them to Sanity document types or page builder blocks.
- “Create a migration plan for my WordPress site.”
- The agent will outline the steps to export WXR data, map WordPress posts to Sanity schemas, convert HTML to Portable Text, and set up redirects.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/sanity-io/sanity-migration/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip sanity-migration.zip -d .agents/skills
Documentation by Sanity
Sanity Migration
Use this skill for CMS-to-Sanity migration work. Treat migration as a content strategy and ETL project, not a blind lift-and-shift.
Required Workflow
Read
references/general.mdfirst.If the source platform is known, also read its guide:
AEM / Adobe Experience Manager:
references/aem.mdContentful:
references/contentful.mdStrapi:
references/strapi.mdWebflow:
references/webflow.mdWordPress / WXR / Elementor:
references/wordpress.mdPayload:
references/payload.mdDrupal:
references/drupal.mdMarkdown / MDX / frontmatter files:
references/markdown.md
Before writing code, produce a short migration plan covering source access, content scope, schema decisions, extraction, transformation, import, validation, redirects, and cutover.
Prefer deterministic, repeatable scripts for real migrations. Write and review migration scripts, mappings, and validation checks; do not rely on one-off content operations for large content volumes.
Deliverables to Produce
For implementation or planning tasks, produce these artifacts or explain why they are not needed:
Content inventory: source types, counts, locales, status/draft scope, assets, and relationship types.
Source-to-Sanity mapping: document types, object types, references, Portable Text fields, asset fields, IDs, and skipped content.
Extraction approach: credentials/access needed, API/export commands, raw snapshot location, and known blind spots.
Transform/import plan: deterministic IDs, write order, asset handling, rich text conversion, validation, and rerun strategy.
Cutover plan: delta sync/content freeze, redirects, broken-link checks, SEO metadata, and manual cleanup.
Defaults
Use stable document IDs derived from source IDs, slugs, paths, or hashes.
Use
createOrReplace,createIfNotExists, orsanity datasets import --replaceso reruns converge.Snapshot extracted source data to disk before transforming it.
Import or create referenced documents before documents that reference them.
Convert rich text to Portable Text instead of storing raw HTML or Markdown strings.
Upload assets to Sanity or the Media Library; do not leave production content dependent on legacy CDN URLs.
Track per-document quality issues and produce a validation summary before cutover.
Preserve legacy URLs and source IDs for redirects, QA, and future debugging.
Sanity Guardrails
Model what content is, not how the old site rendered it.
Use documents for reusable or independently managed entities; use objects for content owned by one document.
Use
defineType,defineField, anddefineArrayMemberif authoring Sanity schemas.Use image/file fields with uploaded Sanity assets or Media Library assets, not legacy CDN URLs.
Use Portable Text arrays for rich text and custom blocks; do not store raw HTML as the canonical body.
Run schema extraction and TypeGen after schema or GROQ query changes when the project uses TypeScript.
Deploy or apply schema changes before using MCP/content tools against the target dataset.
For deeper Sanity implementation guidance, use sanity-best-practices if it is already available. If it is not installed, tell the user they can add it with:
npx skills add sanity-io/agent-toolkit --skill sanity-best-practicesStop and Ask
Stop before coding when any of these are unclear:
Source access path, credentials, export file, or database connection.
Target Sanity project/dataset or whether a scratch dataset should be used.
Draft, archived, scheduled, locale, or version history scope.
Whether media files should be migrated and whether asset URLs/files are accessible.
Whether the destination schema exists or should be designed as part of the migration.
Do Not Do This
Do not create random IDs for source-backed documents.
Do not fetch-then-create referenced documents; use deterministic IDs and
createIfNotExists/createOrReplace.Do not run bulk migrations through MCP content tools when NDJSON or scripts are appropriate.
Do not flatten locale fallback values into translations unless requested.
Do not leave TODOs for required media, authors, references, or rich text conversion.
Do not declare a migration done without count checks, sample checks, reference checks, and route/redirect checks.
Reference Map
Use references/general.md for shared migration principles and the platform references for source-specific extraction routes, modeling traps, and validation checks.
For source systems not explicitly covered, apply references/general.md and adapt the closest platform pattern:
API-first CMSes: start from Contentful, Strapi, or Payload.
Monolithic/page-builder systems: start from WordPress, Drupal, Webflow, or AEM.
HTML-heavy exports: start from the WordPress and Webflow rich-text guidance.
Markdown-first sources: start from
references/markdown.md.
Files
- ATTRIBUTION.mdAdded by AutoGineer863 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md5.3 KiB
- references/aem.md6.8 KiB
- references/contentful.md9.5 KiB
- references/drupal.md4.4 KiB
- references/general.md12.6 KiB
- references/markdown.md5.1 KiB
- references/payload.md7.5 KiB
- references/strapi.md6.3 KiB
- references/webflow.md5.7 KiB
- references/wordpress.md12.5 KiB
references/wordpress.md
WordPress to Sanity
What to Determine First
Before writing migration code, determine:
Source format: REST API, WXR/XML export, WPGraphQL, WP-CLI, database dump, static HTML, or individual page-builder exports.
Whether the site uses Gutenberg blocks, Classic editor HTML, Advanced Custom Fields, custom post types, custom taxonomies, multilingual plugins, WooCommerce, Yoast, or page builders.
Whether drafts, private posts, scheduled posts, revisions, menus, comments, and users are in scope.
Whether media files are public, behind auth, on WordPress uploads, on a CDN, or in a third-party DAM.
Whether WordPress IDs can be preserved in deterministic Sanity IDs.
Extraction Paths
Choose the extraction route by available access:
WordPress REST API: good for live, paginated, public content. Use
/wp-json/wp/v2,per_page=100, and response headers such asx-wp-totalandx-wp-totalpagesfor counts.Authenticated REST API: needed for
context=edit, raw Gutenberg content, drafts, private fields, and some plugin data. Use application passwords or another approved auth flow; store credentials in environment variables.WXR/XML export: common for handoff scenarios and good for a repeatable file-based migration. Parse namespaced fields and unwrap CDATA or parser objects before transformation.
WPGraphQL: useful when installed and configured to expose the needed fields.
WP-CLI/database: useful with direct server access, custom fields, and completeness checks.
Static HTML: fallback only when structured access is unavailable.
If a WXR/XML file is present and complete, default to parsing it rather than requiring WP-CLI access.
REST URLs:
Standard site:
https://<domain>/wp-json/wp/v2.Multisite:
https://<domain>/<site-name>/wp-json/wp/v2.Count check:
curl -sI "<base>/posts?per_page=100".
Authenticated raw content example:
curl -u "$WP_USER:$WP_APP_PASSWORD" \
"<base>/posts?context=edit&per_page=100"If REST is blocked by Cloudflare or security plugins, use one of these before scraping rendered HTML: ask for a WXR export, run WordPress locally with a production database copy, have the customer save JSON from an authenticated browser session, or pass a valid cf_clearance cookie only with explicit approval.
Schema Discovery
With WordPress access, export schema signals before modeling:
wp post-type list --format=json > post-types.json
wp taxonomy list --format=json > taxonomies.json
wp acf export --format=json > acf-fields.jsonIf WP-CLI is unavailable, inspect:
register_post_type()calls in themes/plugins.register_taxonomy()calls.ACF
get_field()usage and exported field groups.Template files such as
single-*.php,archive-*.php, and page templates.wp_posts,wp_postmeta, andwp_term_taxonomywhen database access exists.
ACF defaults:
Text/Textarea maps to
stringortext.WYSIWYG maps to Portable Text.
Image maps to
image.Gallery maps to an array of images.
Repeater maps to an array of objects.
Flexible Content maps to an array of union object types.
Relationship maps to a reference or array of references.
Mapping to Sanity
Posts and pages may become
post,page, or more semantic document types such asarticle,person,location,event, orcaseStudy.Custom post types usually deserve explicit Sanity document types, not a generic
postTypefield.Users often become
authororpersondocuments. Treat them separately from Sanity project members.Categories, tags, and useful custom taxonomies should become reference documents when they power navigation, filtering, SEO, or editorial reuse.
ACF field groups map to Sanity fields, objects, or references depending on reuse and lifecycle.
Yoast and other SEO metadata can map to an
seoobject if the target project uses one.Menus can become navigation singleton documents or frontend configuration, depending on the target architecture.
Use page templates and custom post types as signals for content trapped in presentational structures.
Transformation Notes
Use deterministic IDs such as
post-123,page-123,author-41,category-news, or a project-specific semantic prefix.Create authors and taxonomy documents before posts/pages that reference them.
Build attachment maps before transforming content:
_thumbnail_idpostmeta points to an attachment post.Attachment records contain URLs and metadata such as alt text.
Featured images should become Sanity image fields with
_sanityAssetor uploaded asset references.
Decode HTML entities in titles, excerpts, taxonomy names, and author names.
Store editorial dates in explicit fields such as
publishedAtandmodifiedAt; do not rely on_createdAt/_updatedAtfor source publication dates.Omit empty fields instead of storing empty CDATA objects, empty strings, or
null.Keep a per-document quality log for missing authors, failed media, malformed HTML, unsupported blocks, duplicate slugs, and skipped post types.
WXR/XML Field Map
When parsing WXR/XML, always unwrap parser output before writing Sanity documents. Prefer fast-xml-parser so CDATA is captured consistently:
import {XMLParser} from 'fast-xml-parser'
import {readFile} from 'node:fs/promises'
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '',
cdataPropName: '_cdata',
textNodeName: '_text',
})
const xml = await readFile('wordpress-export.xml', 'utf8')
const parsed = parser.parse(xml)
function text(value: unknown): string {
if (value == null) return ''
if (typeof value === 'string' || typeof value === 'number') return String(value)
if (Array.isArray(value)) return text(value[0])
if (typeof value === 'object') {
const record = value as Record<string, unknown>
return text(record._cdata ?? record._text)
}
return ''
}Run every XML field through a helper like text() before writing Sanity documents. Never store raw parser objects.
Key WXR fields:
title-> title.wp:post_name->slug.current.content:encoded-> body or content source, converted to Portable Text.excerpt:encoded-> excerpt; omit when empty.wp:post_date_gmt->publishedAtor equivalent source publication field.dc:creator-> author lookup key; prefer mapping to WordPress author ID when possible.wp:post_type-> content type routing.wp:post_id-> source ID for deterministic Sanity IDs.wp:postmeta[]-> ACF,_thumbnail_id, Yoast, Elementor, and other plugin data.category[]withdomainattributes -> categories, tags, or custom taxonomies.
Build a flat postmeta map:
const meta: Record<string, string> = {}
for (const item of post['wp:postmeta'] || []) {
const key = text(item['wp:meta_key'])
const value = text(item['wp:meta_value'])
if (key) meta[key] = value
}Featured image is not in the main post body. Build an attachment map from wp:post_type === "attachment" records, then resolve meta["_thumbnail_id"] to {url, alt} and set an image field with _sanityAsset.
Map common Yoast fields only when non-empty:
_yoast_wpseo_title->seo.metaTitle._yoast_wpseo_metadesc->seo.metaDescription._yoast_wpseo_focuskw->seo.focusKeyword._yoast_wpseo_meta-robots-noindex === "1"->seo.noIndex.
Also inspect Rank Math or other SEO plugin postmeta if present; do not assume Yoast is the only SEO source.
Rich Text
WordPress body content is the highest-risk part of most migrations.
Rendered REST HTML (
content.rendered): convert to Portable Text with HTML tooling, but expect loss of block/editor structure.Raw Gutenberg content (
content.rawwithcontext=edit): parse with@wordpress/block-serialization-default-parser, then map each block type to Portable Text blocks or custom objects.Classic editor or WXR HTML: prefer a custom JSDOM traversal when the markup includes WordPress artifacts, shortcodes, inconsistent links, or legacy editor cruft.
@portabletext/block-toolsis acceptable only after testing on real posts and confirming markDefs and custom blocks are correct.Shortcodes: identify project-specific shortcodes before conversion. Some become custom objects; others can be stripped or logged for editorial cleanup.
Inline images: resolve through media/attachment maps when possible; otherwise use
_sanityAssetfrom the discovered URL and log unresolved metadata.
For links, remember that Portable Text annotations live in a block's markDefs; spans reference annotation keys through marks. The link mark definition belongs on the block, not the span.
Before running full conversion, test 3-5 real posts and confirm:
The body is an array of Portable Text blocks, not a string.
No raw HTML tags remain in text spans.
Links have
markDefson the block.Empty blocks are filtered out.
Unsupported shortcodes or custom blocks are logged.
Common Gutenberg mappings:
core/paragraph,core/heading,core/list,core/quote: Portable Text blocks.core/image: image block with resolved attachment or_sanityAsset.core/embed: custom embed object or logged external embed.core/columnsandcore/column: custom Portable Text object or page builder object if layout matters.core/button/core/buttons: CTA object, link annotation, or page-level CTA based on reuse.core/table: custom table object; do not flatten unless table semantics are not needed.core/code/core/preformatted: code block object when code content matters.
Elementor and Page Builders
Elementor stores structured page data in _elementor_data, not in normal REST content.rendered.
Prefer Elementor template JSON exports from WordPress admin or WXR/database access to
_elementor_data.The exported JSON has a root
contentarray of sections, columns, containers, and widgets.Work at section/container level. Classify sections by widget mix and editorial purpose, then map to Sanity page builder objects.
Use
widgetTypeto identify content fields, but avoid creating one Sanity type per low-level widget.If only rendered HTML is available, treat it as a lossy fallback and plan human review.
Rewrite private or environment-specific media URLs to public URLs before using
_sanityAsset.
Common Elementor widget fields:
heading:settings.title,settings.header_size.text-editor:settings.editorHTML.image:settings.imageas object or JSON string with URL/ID/alt.button:settings.text,settings.link,settings._css_classes.counter:settings.ending_number,settings.prefix,settings.suffix,settings.title.icon-list:settings.icon_list[].html:settings.html; inspect for forms, carousels, or embeds.template:settings.template_id; resolve global Elementor templates separately.
Other page builders such as Divi and Beaver Builder have similar risks: rendered HTML loses source structure and usually requires custom handling.
Gotchas
REST API access can be blocked by security plugins, Cloudflare, or custom hardening.
WXR/XML parser output can wrap values in objects such as
_cdata; unwrap every field before writing Sanity documents.content.renderedhas shortcodes and filters already applied;content.rawrequires auth but preserves Gutenberg block comments.ACF fields may not appear in REST unless configured.
Author display names are fragile join keys. Prefer WordPress author IDs when available.
Attachment URLs may point to staging domains, private CDN domains, or resized variants. Prefer the best available original asset URL.
Multilingual plugins differ significantly; inspect the plugin-specific relationship data before choosing Sanity's localization shape.
Validation Checklist
Count documents by
wp:post_typeor REST endpoint and compare to Sanity document counts.Confirm skipped WordPress types are intentional, such as
attachment,nav_menu_item,wp_block, or ACF definitions.Spot-check at least five rich text documents across old/new content and different editors.
Search generated output for raw
<p,_cdata, and[object Object].Confirm featured images resolve from
_thumbnail_idor another source-specific image field.Confirm author and taxonomy references point to existing documents.
Confirm at least one document has a real image
_sanityAssetdirective or uploaded asset reference, not a TODO/null placeholder.Confirm the author pass uses WordPress author IDs or a documented fallback map, not display-name matching alone.
Confirm Yoast/SEO objects are omitted when all source values are empty.
Verify old URLs, slugs, canonical metadata, and redirects.
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs Node.js
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- Sanity
- Publisher
- Sanity (Official)
- Source
- sanity-io/agent-toolkit · skills/sanity-migration · commit 3ed3a14
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Sanity.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download sanity-migration.zip