Three weeks ago I
v0.4.0 — to-import closed the migration loop
The first big gap I hit after v0.3.0 was the silent one between "the simulation looks right" and "now actually run the migration in Contentful." cms-sim simulate produced a beautiful preview, but exporting that to a Contentful-importable shape was still on the user.
cms-sim to-import converts a simulation output directory into the JSON bundle that the official contentful-import CLI consumes. Validations, default values, link references, RichText, locales — all the bits Contentful's importer expects, generated from the simulator's already-validated model.
# Simulate locally
npx cms-sim --schemas=schemas/ --input=data/export.ndjson --output=output/
# Convert simulator output → contentful-import bundle
npx cms-sim to-import --input=output/ --schemas=schemas/ --output=bundle/
# Run the actual migration with Contentful's own tool (cms-sim never touches your space)
npx contentful-import --content-file=bundle/contentful.json --space-id=YOUR_SPACE --management-token=YOUR_CMA
The bundle gets validated against contentful-import's own Joi schema before it's written, so a simulation that passes locally produces a payload that passes the importer too. By default everything's marked as draft; pass --publish and the entries / assets ship as published.
This is the piece that turned cms-sim from "a preview tool" into "the preview half of a real migration workflow."
v0.5.0 — visual HTML diff + 8 fixes that only dogfood could surface
The next thing I needed was a way to see what changed between two simulations. I had cms-sim diff already, but it was JSON-only. Reading a diff JSON to compare two content models is a job for nobody.
cms-sim diff --html --open renders both schema-only and full-report diffs as a self-contained HTML page with KPI cards (added / removed / changed content types, entry-count deltas, error and warning gain or loss), collapsible per-content-type panels with field-level changes, color-coded badges for added / removed / changed / reordered fields, and an entry-count table with side-by-side bars. Zero external assets — open the file with file:// and it works.
v0.6.1 — pull-wordpress (the larger audience)
A week later, the same approach landed for WordPress. cms-sim pull-wordpress reads a WXR XML export (wp-admin → Tools → Export → All content) and writes the same Contentful-shape output:
npx cms-sim pull-wordpress --input=wp-export.xml --output=pulled-wp/
npx cms-sim --schemas=pulled-wp/schemas/ --input=pulled-wp/data/entries.ndjson --open
WordPress is messier than Sanity because the references aren't structured the same way:
Authors come from<dc:creator>as login strings, not refs.
Categories / tags come from inline<category>elements withnicenameslugs.
Featured images live in<wp:postmeta>under the_thumbnail_idkey as a post_id pointer.
Bodies are Gutenberg-flavored HTML (block comments stripped, but the inner markup stays).
Locales (if Polylang is installed) come from alanguagetaxonomy on each item.
pull-wordpress handles all of those: post categories[] slugs → Array<Link Entry> with linkContentType: ['category'] validations derived from the real refs, _thumbnail_id → featuredImage: { sys: Link Asset }, attachments extracted into assets/assets.json, body HTML converted to Contentful RichText via the existing htmlToRichText walker, and per-doc Polylang locale tags surfaced in contentful-space.json.
Stable id prefixes per content type — wp_<postId>, wp_author_<login>, wp_category_<slug>, wp_tag_<slug> — so cross-references resolve without lookup tables and the asset id in entries.ndjson matches the asset id in assets.json.
Anonymization is fine and encouraged. Replace post bodies with lorem ipsum, scrub user emails and display names, swap your domain in <wp:base_site_url> and <wp:attachment_url>. What matters is the structure — the <wp:post_type> declarations, the <wp:postmeta> keys (ACF / WooCommerce / Polylang plugin conventions), the category and language taxonomy slugs, and the Gutenberg block markers inside <content:encoded>. The content can be whatever.
What you get back if you contribute:
- Your anonymized export becomes a checked-in test fixture under
src/wordpress/**/*.test.tsorexample-wp-pull/data/. - The release notes for whatever patch fixes the edge cases you surface will credit your handle (unless you ask me not to).
- If your export uncovers a non-trivial bug, you get co-author credit on the fix commit.
I'm going to let v0.6.1 sit for a couple of weeks specifically to see what the community surfaces. Whatever lands in issue #15 informs what v0.6.2 ships.
Where the project stands today
Current version:v0.6.1—npm install content-model-simulator
Test suite: 659 passing, zero runtime dependencies
Adapters shipped: Contentful (pull), Sanity (pull-sanity), WordPress (pull-wordpress), plusfrom-migrationsforcontentful-migrationscript replay
Bridge to the real thing:to-importexports acontentful-import-ready bundle
What's next: v0.7.x plugin system polish, then v0.9.0 API stabilization, then v1.0.0
The thing I'm still optimizing for is the same as v0.3: reduce stupid risk before it reaches a real Contentful space. Every release in this cycle has been a step toward making more of the work you'd otherwise do against a live space doable offline first, with structured feedback you can actually read.
If you work with Contentful and any of this sounds relevant — pull or migration planning, validating a model before it ships, comparing two iterations of a content type — give it a try. Open an issue if it breaks. Drop a WXR on issue #15 if you can.
Repo: https://github.com/JoshuaPozos/content-model-simulator
Package: https://www.npmjs.com/package/content-model-simulator
WordPress dogfood issue: https://github.com/JoshuaPozos/content-model-simulator/issues/15

SOCIAL SHARE CARD GENERATOR