<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Joshua Pozos</title>
    <description>The latest articles on DEV Community by Joshua Pozos (@joshuapozos).</description>
    <link>https://dev.to/joshuapozos</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F459915%2Fe869f24f-a335-49ea-b811-c4392ba120cf.jpeg</url>
      <title>DEV Community: Joshua Pozos</title>
      <link>https://dev.to/joshuapozos</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/joshuapozos"/>
    <language>en</language>
    <item>
      <title>Four cms-sim releases in three weeks — to-import bundles, visual diff, and one-command pulls from Sanity + WordPress</title>
      <dc:creator>Joshua Pozos</dc:creator>
      <pubDate>Sun, 10 May 2026 22:24:53 +0000</pubDate>
      <link>https://dev.to/joshuapozos/four-cms-sim-releases-in-three-weeks-to-import-bundles-visual-diff-and-one-command-pulls-from-p65</link>
      <guid>https://dev.to/joshuapozos/four-cms-sim-releases-in-three-weeks-to-import-bundles-visual-diff-and-one-command-pulls-from-p65</guid>
      <description>&lt;p&gt;Three weeks ago I &lt;a href="https://dev.to/joshuapozos/i-built-a-local-contentful-model-simulator-to-stop-testing-content-models-blindly-n4k"&gt;posted about content-model-simulator&lt;/a&gt; at &lt;strong&gt;v0.3.0&lt;/strong&gt;. The pitch was simple: stop testing Contentful content models blindly. Define schemas locally, preview them in a browser, catch the bad field design before editorial has 200 entries in it.&lt;/p&gt;

&lt;p&gt;Today the package is at &lt;strong&gt;v0.6.1&lt;/strong&gt;. That's four minor releases in three weeks, all pre-1.0, all shipped publicly the moment they were ready instead of bundled into one big drop. Here's what landed, what I learned about the cadence, and one specific thing I need from anyone reading this.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvj9xmwe5b0pb9pz8lovi.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvj9xmwe5b0pb9pz8lovi.webp" alt="cms-sim v0.6.1 page on npmjs.com showing the package as Public with the install command npm i content-model-simulator, a link to the github repo, and a row of badges including npm version, node 22 minimum, 659 tests passing, zero runtime dependencies, and MIT license" width="800" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  v0.4.0 — &lt;code&gt;to-import&lt;/code&gt; closed the migration loop
&lt;/h2&gt;

&lt;p&gt;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." &lt;code&gt;cms-sim simulate&lt;/code&gt; produced a beautiful preview, but exporting that to a Contentful-importable shape was still on the user.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cms-sim to-import&lt;/code&gt; converts a simulation output directory into the JSON bundle that the official &lt;code&gt;contentful-import&lt;/code&gt; CLI consumes. Validations, default values, link references, RichText, locales — all the bits Contentful's importer expects, generated from the simulator's already-validated model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Simulate locally&lt;/span&gt;
npx cms-sim &lt;span class="nt"&gt;--schemas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;schemas/ &lt;span class="nt"&gt;--input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;data/export.ndjson &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;output/

&lt;span class="c"&gt;# Convert simulator output → contentful-import bundle&lt;/span&gt;
npx cms-sim to-import &lt;span class="nt"&gt;--input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;output/ &lt;span class="nt"&gt;--schemas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;schemas/ &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bundle/

&lt;span class="c"&gt;# Run the actual migration with Contentful's own tool (cms-sim never touches your space)&lt;/span&gt;
npx contentful-import &lt;span class="nt"&gt;--content-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bundle/contentful.json &lt;span class="nt"&gt;--space-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_SPACE &lt;span class="nt"&gt;--management-token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_CMA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bundle gets validated against &lt;code&gt;contentful-import&lt;/code&gt;'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 &lt;strong&gt;draft&lt;/strong&gt;; pass &lt;code&gt;--publish&lt;/code&gt; and the entries / assets ship as published.&lt;/p&gt;

&lt;p&gt;This is the piece that turned cms-sim from "a preview tool" into "the preview half of a real migration workflow."&lt;/p&gt;

&lt;h2&gt;
  
  
  v0.5.0 — visual HTML diff + 8 fixes that only dogfood could surface
&lt;/h2&gt;

&lt;p&gt;The next thing I needed was a way to see &lt;strong&gt;what changed&lt;/strong&gt; between two simulations. I had &lt;code&gt;cms-sim diff&lt;/code&gt; already, but it was JSON-only. Reading a diff JSON to compare two content models is a job for nobody.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cms-sim diff --html --open&lt;/code&gt; 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 &lt;code&gt;file://&lt;/code&gt; and it works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3emz3vbj31n3xzgsw2v5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3emz3vbj31n3xzgsw2v5.png" alt="Schema diff rendered as HTML by cms-sim diff html flag — three KPI cards across the top: zero content types added, zero content types removed, one content type changed; below them a Schema changes panel marks the author content type as CHANGED with two added fields listed in green, expertiseTags and yearsOfExperience" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The more interesting half of v0.5.0 was an unplanned batch of &lt;strong&gt;8 UX fixes&lt;/strong&gt; that came out of dogfooding the package against a production Contentful space with 23 content types and 9,246 entries. Some highlights:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The simulator was silently filtering content types with zero entries from the report, which meant a freshly added empty CT vanished from the model graph the moment you ran &lt;code&gt;diff&lt;/code&gt;. Fixed.&lt;/li&gt;
&lt;li&gt;The content browser was hardcoding the entry display name to &lt;code&gt;internalName &amp;gt; title &amp;gt; name &amp;gt; lblTitle &amp;gt; id&lt;/code&gt; instead of reading the schema's declared &lt;code&gt;displayField&lt;/code&gt;. With auto-generated IDs in &lt;code&gt;internalName&lt;/code&gt;, the browser was showing &lt;code&gt;pillarpage-5x3cm7...&lt;/code&gt; instead of the human-readable titles. Fixed.&lt;/li&gt;
&lt;li&gt;Safari and Chrome restore &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; form state across reloads of the same file path. A user who picked "Pillar Page" once saw an empty list on every subsequent load if the new dataset didn't have that CT. &lt;code&gt;autocomplete="off"&lt;/code&gt; + explicit reset on init.&lt;/li&gt;
&lt;li&gt;Synchronous render of thousands of entries blocked the browser before it could paint the loading state. Defer with &lt;code&gt;requestAnimationFrame&lt;/code&gt; × 2 — one to let the spinner paint, one to start the heavy work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern I noticed: every one of these bugs was a filter or default that "cleaned up" the output in a way that hid real information. Whenever I saw &lt;code&gt;entryCount &amp;gt; 0&lt;/code&gt; as a filter going forward, I started suspecting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  v0.6.0 — &lt;code&gt;pull-sanity&lt;/code&gt; (the harder source)
&lt;/h2&gt;

&lt;p&gt;The roadmap I'd written had v0.6.0 listed as "Multi-CMS adapter — prove the architecture is CMS-agnostic." When I sat down to start it, I realized this was the wrong direction. The package is explicitly positioned as &lt;strong&gt;the offline Contentful simulator&lt;/strong&gt; — the README literally says "Stop designing Contentful models blind" and "Who this is NOT for: Non-Contentful platforms." Building a multi-CMS adapter would have contradicted the whole product positioning.&lt;/p&gt;

&lt;p&gt;The real gap was different. Today, the package already pulled from Contentful (&lt;code&gt;cms-sim pull&lt;/code&gt;) and could preview migrations from WordPress XML or Sanity NDJSON — but only if the user wrote a &lt;code&gt;transforms/&lt;/code&gt; directory by hand to map their source data into Contentful's shape. That's exactly the work &lt;code&gt;cms-sim pull&lt;/code&gt; automates for Contentful sources. It should automate it for non-Contentful sources too.&lt;/p&gt;

&lt;p&gt;So v0.6.0 became &lt;strong&gt;&lt;code&gt;cms-sim pull-sanity&lt;/code&gt;&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Export your Sanity dataset (their CLI, not ours)&lt;/span&gt;
sanity dataset &lt;span class="nb"&gt;export &lt;/span&gt;production export.ndjson

&lt;span class="c"&gt;# Convert to Contentful shape (offline, zero deps, read-only)&lt;/span&gt;
npx cms-sim pull-sanity &lt;span class="nt"&gt;--input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;export.ndjson &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pulled-sanity/

&lt;span class="c"&gt;# Now everything downstream works the same as with `cms-sim pull` from a Contentful space&lt;/span&gt;
npx cms-sim &lt;span class="nt"&gt;--schemas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pulled-sanity/schemas/ &lt;span class="nt"&gt;--input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pulled-sanity/data/entries.ndjson &lt;span class="nt"&gt;--open&lt;/span&gt;
npx cms-sim to-import &lt;span class="nt"&gt;--input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;output/ &lt;span class="nt"&gt;--schemas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pulled-sanity/schemas/ &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bundle/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this command does in one pass:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Infers a content type schema&lt;/strong&gt; from real document samples (Symbol / Text / Integer / Number / Boolean / Date / Object / RichText / Link Entry / Link Asset / Array variants), with &lt;code&gt;linkContentType&lt;/code&gt; validations derived from the actual cross-document references in the corpus.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewrites &lt;code&gt;_ref&lt;/code&gt; references&lt;/strong&gt; (single + nested in arrays) to Contentful's &lt;code&gt;Link Entry&lt;/code&gt; sys shape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewrites image references&lt;/strong&gt; (&lt;code&gt;{_type: 'image', asset: {_ref}}&lt;/code&gt;) to &lt;code&gt;Link Asset&lt;/code&gt; + emits an &lt;code&gt;assets/assets.json&lt;/code&gt; index.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Converts Portable Text&lt;/strong&gt; (Sanity's structured rich-text format) into Contentful RichText documents — paragraph / heading / lists / blockquote / decorator marks / hyperlinks all map. Unknown marks emit explicit warnings while preserving the text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detects locale-shaped values&lt;/strong&gt; (&lt;code&gt;{en: '…', es: '…'}&lt;/code&gt;) and fans each document out into one variant per locale, with the matching schema fields flagged &lt;code&gt;localized: true&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;End-to-end against the production Sanity export I use as my dogfood dataset (21 docs / 7 content types / 15 assets / 2 locales / Portable Text bodies): &lt;strong&gt;pipeline warnings dropped from 97 against the raw NDJSON to 0 after &lt;code&gt;pull-sanity&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Running it against that dataset looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;════════════════════════════════════════════════════════════════════
Content Model Simulator — Pull Sanity
════════════════════════════════════════════════════════════════════

Reading Sanity NDJSON export (offline)…

  Read 21 document(s), 15 asset(s) from .../production.ndjson
  1 inference warning(s):
    • [vehicle.availableFeatures] Contentful arrays only support Symbol/Link items — collapsed to Object.
  Wrote 7 schema(s) + 37 entry/entries (2 locales) + 15 asset(s) to ./pulled-sanity

✓ 7 content type(s) detected
✓ 37 entry/entries written     (21 docs × 2 locales, deduped where missing)
✓ 15 asset(s) detected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the resulting content browser, with a multi-locale post (English + Spanish bodies rendered as Contentful RichText nodes side-by-side, references resolved to actual entries, and the field-type badges showing &lt;code&gt;RichText&lt;/code&gt; / &lt;code&gt;Object&lt;/code&gt; / &lt;code&gt;Date&lt;/code&gt; / &lt;code&gt;Link Entry&lt;/code&gt; as inferred):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F091sdxvl76kvugba4tuk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F091sdxvl76kvugba4tuk.png" alt="Content Browser for the pull-sanity output of a Sanity dataset, showing 21 entries across seven content types (Post, Tags, Feature, Category Blog, Vehicle, Available Feature, Vehicle Category) with two locale chips on each entry (en and es); the right pane shows the Fleet Optimization post with both English and Spanish bodies rendered as Contentful RichText, including heading-5 nodes, paragraphs, bulleted lists, and hyperlinks" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  v0.6.1 — &lt;code&gt;pull-wordpress&lt;/code&gt; (the larger audience)
&lt;/h2&gt;

&lt;p&gt;A week later, the same approach landed for WordPress. &lt;code&gt;cms-sim pull-wordpress&lt;/code&gt; reads a WXR XML export (&lt;code&gt;wp-admin → Tools → Export → All content&lt;/code&gt;) and writes the same Contentful-shape output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx cms-sim pull-wordpress &lt;span class="nt"&gt;--input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;wp-export.xml &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pulled-wp/
npx cms-sim &lt;span class="nt"&gt;--schemas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pulled-wp/schemas/ &lt;span class="nt"&gt;--input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pulled-wp/data/entries.ndjson &lt;span class="nt"&gt;--open&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WordPress is messier than Sanity because the references aren't structured the same way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authors&lt;/strong&gt; come from &lt;code&gt;&amp;lt;dc:creator&amp;gt;&lt;/code&gt; as login strings, not refs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Categories / tags&lt;/strong&gt; come from inline &lt;code&gt;&amp;lt;category&amp;gt;&lt;/code&gt; elements with &lt;code&gt;nicename&lt;/code&gt; slugs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Featured images&lt;/strong&gt; live in &lt;code&gt;&amp;lt;wp:postmeta&amp;gt;&lt;/code&gt; under the &lt;code&gt;_thumbnail_id&lt;/code&gt; key as a post_id pointer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bodies&lt;/strong&gt; are Gutenberg-flavored HTML (block comments stripped, but the inner markup stays).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Locales&lt;/strong&gt; (if Polylang is installed) come from a &lt;code&gt;language&lt;/code&gt; taxonomy on each item.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;pull-wordpress&lt;/code&gt; handles all of those: post &lt;code&gt;categories[]&lt;/code&gt; slugs → &lt;code&gt;Array&amp;lt;Link Entry&amp;gt;&lt;/code&gt; with &lt;code&gt;linkContentType: ['category']&lt;/code&gt; validations derived from the real refs, &lt;code&gt;_thumbnail_id&lt;/code&gt; → &lt;code&gt;featuredImage: { sys: Link Asset }&lt;/code&gt;, attachments extracted into &lt;code&gt;assets/assets.json&lt;/code&gt;, body HTML converted to Contentful RichText via the existing &lt;code&gt;htmlToRichText&lt;/code&gt; walker, and per-doc Polylang locale tags surfaced in &lt;code&gt;contentful-space.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Stable id prefixes per content type — &lt;code&gt;wp_&amp;lt;postId&amp;gt;&lt;/code&gt;, &lt;code&gt;wp_author_&amp;lt;login&amp;gt;&lt;/code&gt;, &lt;code&gt;wp_category_&amp;lt;slug&amp;gt;&lt;/code&gt;, &lt;code&gt;wp_tag_&amp;lt;slug&amp;gt;&lt;/code&gt; — so cross-references resolve without lookup tables and the asset id in &lt;code&gt;entries.ndjson&lt;/code&gt; matches the asset id in &lt;code&gt;assets.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flxt3d1vygqbd46u7f18x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flxt3d1vygqbd46u7f18x.png" alt="Content Browser for the pull-wordpress output of a synthetic WXR export with 11 entries across five content types (author, category, tag, post, page) and a single locale; the right pane shows the Building cms-sim in TypeScript post in Editor view with Title and Slug as Symbol fields, Body badged as RichText showing a paragraph that reads — We migrated our blog from WordPress to Contentful using cms-sim — with bold and italic marks and a hyperlink, a heading-2 reading Why this works, and a three-item bulleted list (One command per source, Zero runtime deps, Offline read-only); Excerpt is RichText, Publish Date is a Date, Status is a Symbol, and Author is a Link resolving to the author-aliciaw entry" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned shipping four minors in three weeks
&lt;/h2&gt;

&lt;p&gt;A few things stuck with me.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Ship per-source, not all at once
&lt;/h3&gt;

&lt;p&gt;The original roadmap had Sanity and WordPress bundled as "v0.6.0 multi-CMS adapter." Splitting them into v0.6.0 + v0.6.1 was the right call. Real users of &lt;code&gt;pull-sanity&lt;/code&gt; will surface bugs that no synthetic fixture catches, and those bugs feed back into the WordPress design instead of being baked into a single shipped release I can't unship.&lt;/p&gt;

&lt;p&gt;Pre-1.0 SemVer is built for exactly this rhythm. MINOR bumps add features; PATCH bumps fix them. Bundling delays releases by weeks for no real benefit — there's no installed base getting "release fatigue" yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The simulator's own warning count is the best progress metric
&lt;/h3&gt;

&lt;p&gt;For every milestone of &lt;code&gt;pull-sanity&lt;/code&gt; and &lt;code&gt;pull-wordpress&lt;/code&gt;, I tracked one number: how many warnings does the simulator emit against the real dataset? It started at 97 for the link-vehicles Sanity export and went to 0 across five milestones (schema inference → 13, ref rewriting → 13, RichText body → 13, asset linking → 0). That was more useful than any unit test for guiding what to build next.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Doc consistency is harder than code consistency
&lt;/h3&gt;

&lt;p&gt;I shipped v0.6.0 with a hardcoded &lt;code&gt;tests 510 passing&lt;/code&gt; badge that was actually 601 by then. Nobody noticed before the publish. I added a permanent rule to my own development docs to check the badge before every commit. The tools-don't-update-themselves problem is real even for small repos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Help wanted — real WordPress data, especially with ACF
&lt;/h2&gt;

&lt;p&gt;This is the special section.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pull-wordpress&lt;/code&gt; shipped in v0.6.1 with &lt;strong&gt;synthetic WXR fixtures only&lt;/strong&gt;. The unit tests cover the shapes I could construct from scratch (Gutenberg + Classic bodies, nested categories, featured images, Polylang per-doc locales), and the &lt;code&gt;example-wp-pull/&lt;/code&gt; walk-through exercises the happy path against 12 synthetic documents. That's enough to prove the structure works, but it almost certainly misses edge cases that only show up in real production exports.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I'd love to dogfood against:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ACF (Advanced Custom Fields)&lt;/strong&gt; field groups with repeaters, flexible content, post-object refs, gallery / image fields, conditional logic. ACF is the single biggest gap right now — the synthetic fixture doesn't exercise it at all, and ACF is in 60%+ of serious WordPress sites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Polylang or WPML with real translation grouping&lt;/strong&gt; — the &lt;code&gt;_translations&lt;/code&gt; post-meta key that links en/fr/es versions of the same post into one logical entry. The current adapter treats each translation as a separate entry tagged with its own locale, which is correct as a starting point but loses the multi-locale-on-one-entry shape Contentful prefers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WooCommerce&lt;/strong&gt; product structures (product attributes, variations, custom taxonomies, gallery images).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom post types&lt;/strong&gt; + ACF combos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gutenberg blocks beyond the basics&lt;/strong&gt; — custom blocks, embeds, columns, reusable blocks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you maintain a WordPress site and can share the file from &lt;code&gt;wp-admin → Tools → Export → All content&lt;/code&gt;, please drop it on the tracking issue:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/JoshuaPozos/content-model-simulator/issues/15" rel="noopener noreferrer"&gt;https://github.com/JoshuaPozos/content-model-simulator/issues/15&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anonymization is fine and encouraged.&lt;/strong&gt; Replace post bodies with lorem ipsum, scrub user emails and display names, swap your domain in &lt;code&gt;&amp;lt;wp:base_site_url&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;wp:attachment_url&amp;gt;&lt;/code&gt;. What matters is the &lt;strong&gt;structure&lt;/strong&gt; — the &lt;code&gt;&amp;lt;wp:post_type&amp;gt;&lt;/code&gt; declarations, the &lt;code&gt;&amp;lt;wp:postmeta&amp;gt;&lt;/code&gt; keys (ACF / WooCommerce / Polylang plugin conventions), the category and language taxonomy slugs, and the Gutenberg block markers inside &lt;code&gt;&amp;lt;content:encoded&amp;gt;&lt;/code&gt;. The content can be whatever.&lt;/p&gt;

&lt;p&gt;What you get back if you contribute:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your anonymized export becomes a checked-in test fixture under &lt;code&gt;src/wordpress/**/*.test.ts&lt;/code&gt; or &lt;code&gt;example-wp-pull/data/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The release notes for whatever patch fixes the edge cases you surface will credit your handle (unless you ask me not to).&lt;/li&gt;
&lt;li&gt;If your export uncovers a non-trivial bug, you get co-author credit on the fix commit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the project stands today
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Current version:&lt;/strong&gt; &lt;code&gt;v0.6.1&lt;/code&gt; — &lt;code&gt;npm install content-model-simulator&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test suite:&lt;/strong&gt; 659 passing, zero runtime dependencies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adapters shipped:&lt;/strong&gt; Contentful (&lt;code&gt;pull&lt;/code&gt;), Sanity (&lt;code&gt;pull-sanity&lt;/code&gt;), WordPress (&lt;code&gt;pull-wordpress&lt;/code&gt;), plus &lt;code&gt;from-migrations&lt;/code&gt; for &lt;code&gt;contentful-migration&lt;/code&gt; script replay&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bridge to the real thing:&lt;/strong&gt; &lt;code&gt;to-import&lt;/code&gt; exports a &lt;code&gt;contentful-import&lt;/code&gt;-ready bundle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's next:&lt;/strong&gt; v0.7.x plugin system polish, then v0.9.0 API stabilization, then v1.0.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The thing I'm still optimizing for is the same as v0.3: &lt;strong&gt;reduce stupid risk before it reaches a real Contentful space.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw0p77qryhwp1wbomndpk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw0p77qryhwp1wbomndpk.png" alt="Content Model Graph for the pull-wordpress output; stats badges across the top read 5 types, 11 entries, 0 assets, 1 locales, 0 errors, and 2 warnings; the canvas shows five content type cards (Post, Page, Category, Tag, Author) connected by directional arrows labeled categories (Post to Category), tags (Post to Tag), author (Post to Author), and author (Page to Author); the right side panel lists each content type with entry and field counts" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;code&gt;https://github.com/JoshuaPozos/content-model-simulator&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Package:&lt;/strong&gt; &lt;code&gt;https://www.npmjs.com/package/content-model-simulator&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;WordPress dogfood issue:&lt;/strong&gt; &lt;code&gt;https://github.com/JoshuaPozos/content-model-simulator/issues/15&lt;/code&gt;&lt;/p&gt;

</description>
      <category>devtools</category>
      <category>contentful</category>
      <category>typescript</category>
      <category>issue</category>
    </item>
    <item>
      <title>I built a local Contentful model simulator to stop testing content models blindly</title>
      <dc:creator>Joshua Pozos</dc:creator>
      <pubDate>Sat, 18 Apr 2026 21:54:24 +0000</pubDate>
      <link>https://dev.to/joshuapozos/i-built-a-local-contentful-model-simulator-to-stop-testing-content-models-blindly-n4k</link>
      <guid>https://dev.to/joshuapozos/i-built-a-local-contentful-model-simulator-to-stop-testing-content-models-blindly-n4k</guid>
      <description>&lt;p&gt;When you work with Contentful long enough, you start noticing how much model work is still too easy to do blindly.&lt;/p&gt;

&lt;p&gt;You change a field, push it, inspect it in a real space, realize something is off, undo it, tweak it again, and repeat. It gets worse when you are planning a migration and you cannot really see how the content will look until you import it into Contentful and deal with the fallout there. That loop is slow, clumsy, and riskier than it should be.&lt;/p&gt;

&lt;p&gt;That is why I started building &lt;strong&gt;content-model-simulator&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is not finished yet. This is still a work in progress. But &lt;strong&gt;v0.3.0 is already out&lt;/strong&gt;, and my target is to reach &lt;strong&gt;v1.0.0 by the end of May&lt;/strong&gt;. The point of this post is not to pretend the tool is done. The point is to show the problem I am trying to solve, what already works, and what I have learned while building it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3m89c70vy226zta73j5o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3m89c70vy226zta73j5o.png" alt="Content browser showing entries list, field panel, and a linked reference" width="800" height="591"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;I do not like testing content models directly in a real Contentful space unless I absolutely have to.&lt;/p&gt;

&lt;p&gt;For simple changes, maybe that is fine. But once the model starts getting more connected, localized, or migration-heavy, working directly against a live space becomes awkward. You are not really designing anymore. You are poking the system and hoping the shape in your head matches the shape in the CMS.&lt;/p&gt;

&lt;p&gt;And the pain is not only about content types.&lt;/p&gt;

&lt;p&gt;It is also about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;seeing how entries will actually look&lt;/li&gt;
&lt;li&gt;spotting broken assumptions before import&lt;/li&gt;
&lt;li&gt;validating references&lt;/li&gt;
&lt;li&gt;checking how localized content behaves&lt;/li&gt;
&lt;li&gt;understanding whether an existing migration script really produces the model you think it does&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the gap I wanted to attack.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am building
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;content-model-simulator&lt;/code&gt; is a local-first simulator for Contentful content models.&lt;/p&gt;

&lt;p&gt;The core idea is simple: define schemas locally, point the tool at real data or mock data, and inspect the result in a browser before touching your real Contentful space. The project is explicitly positioned as a &lt;strong&gt;simulation tool&lt;/strong&gt;, not a migration runner, and its README is pretty clear about that boundary.&lt;/p&gt;

&lt;p&gt;Today, it already supports things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;offline simulation of content types, entries, references, and validation errors&lt;/li&gt;
&lt;li&gt;a Content Browser UI to inspect entries and follow references&lt;/li&gt;
&lt;li&gt;a Content Model Graph to visualize relationships&lt;/li&gt;
&lt;li&gt;mock data generation for designing from scratch&lt;/li&gt;
&lt;li&gt;pulling a content model from Contentful for local preview&lt;/li&gt;
&lt;li&gt;migration preview from external sources like WordPress XML or Sanity NDJSON&lt;/li&gt;
&lt;li&gt;CI-friendly validation&lt;/li&gt;
&lt;li&gt;converting existing &lt;code&gt;contentful-migration&lt;/code&gt; scripts into local schemas with &lt;code&gt;from-migrations&lt;/code&gt;, which landed in &lt;strong&gt;v0.3.0&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmsppabk2siycqfol2xvi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmsppabk2siycqfol2xvi.png" alt="Content model graph with multiple content types and relationships" width="800" height="591"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it this way
&lt;/h2&gt;

&lt;p&gt;I did not want another tool that only sounded useful in theory.&lt;/p&gt;

&lt;p&gt;I wanted something that matched a real workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;define or pull a model&lt;/li&gt;
&lt;li&gt;inspect it locally&lt;/li&gt;
&lt;li&gt;preview entries&lt;/li&gt;
&lt;li&gt;validate the structure&lt;/li&gt;
&lt;li&gt;reduce the chance of breaking something once the real migration or implementation starts&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is the part I care about most: &lt;strong&gt;reducing stupid risk before it reaches a real space&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In other words, this is less about “look, I built a package” and more about “I am trying to make Contentful model work less fragile.”&lt;/p&gt;

&lt;h2&gt;
  
  
  What v0.3.0 already gave me
&lt;/h2&gt;

&lt;p&gt;The new piece in &lt;strong&gt;v0.3.0&lt;/strong&gt; that I am especially happy about is &lt;code&gt;from-migrations&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A lot of teams already use &lt;code&gt;contentful-migration&lt;/code&gt; files as the source of truth for their models. So instead of forcing people to manually rewrite that thinking into another format, I wanted a way to convert migration scripts into simulator schemas and preview them locally. The current changelog describes &lt;code&gt;from-migrations&lt;/code&gt; as a subcommand that converts &lt;code&gt;contentful-migration&lt;/code&gt; scripts into &lt;code&gt;cms-sim&lt;/code&gt; schema files without needing a Contentful connection.&lt;/p&gt;

&lt;p&gt;That matters because it moves the tool closer to a real engineering workflow, not just a demo workflow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Convert your existing contentful-migration files to cms-sim schemas&lt;/span&gt;
npx cms-sim from-migrations &lt;span class="nt"&gt;--migrations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./migrations/ &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./schemas/

&lt;span class="c"&gt;# Preview the resulting content model locally&lt;/span&gt;
npx cms-sim &lt;span class="nt"&gt;--schemas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./schemas/ &lt;span class="nt"&gt;--open&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your migration files are TypeScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx tsx &lt;span class="si"&gt;$(&lt;/span&gt;which cms-sim&lt;span class="si"&gt;)&lt;/span&gt; from-migrations &lt;span class="nt"&gt;--migrations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./migrations/ &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./schemas/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it is not
&lt;/h2&gt;

&lt;p&gt;This tool does &lt;strong&gt;not&lt;/strong&gt; replace Contentful.&lt;/p&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; upload, create, or modify anything in your space during simulation. It does &lt;strong&gt;not&lt;/strong&gt; run the actual migration for you either. The package README is explicit: this is a simulator, not a migration tool. The goal is to help you inspect, validate, and think more clearly before you use Contentful’s own tooling for the real operation.&lt;/p&gt;

&lt;p&gt;That distinction matters. I would rather be precise than oversell it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned while building it
&lt;/h2&gt;

&lt;p&gt;A few things became obvious once I started building this.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Content modeling is not only structure, it is behavior
&lt;/h3&gt;

&lt;p&gt;It is easy to think a content model is just fields and types. It is not.&lt;/p&gt;

&lt;p&gt;The real pain appears when you factor in references, locales, validation rules, migration paths, editor expectations, and how actual content will sit inside the model. A model can look fine on paper and still be terrible in practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Preview changes how you think
&lt;/h3&gt;

&lt;p&gt;The moment you can inspect entries and relationships locally, the conversation changes.&lt;/p&gt;

&lt;p&gt;You stop arguing only at the schema level. You start noticing editorial friction, broken assumptions, weird relationships, and validation gaps earlier. That is where the value is.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Existing migration scripts are too valuable to ignore
&lt;/h3&gt;

&lt;p&gt;If a team already invested in &lt;code&gt;contentful-migration&lt;/code&gt;, that work should not be trapped in a one-way execution path. Being able to extract value from those scripts for local preview feels like the right direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick example of the intended workflow
&lt;/h2&gt;

&lt;p&gt;This is the kind of loop I want the tool to support:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;content-model-simulator

&lt;span class="c"&gt;# Scaffold a new project with example schemas&lt;/span&gt;
npx cms-sim init

&lt;span class="c"&gt;# Simulate and open the Content Browser&lt;/span&gt;
npx cms-sim &lt;span class="nt"&gt;--schemas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;schemas/ &lt;span class="nt"&gt;--open&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, once you already have migration scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Convert contentful-migration files → local schemas&lt;/span&gt;
npx cms-sim from-migrations &lt;span class="nt"&gt;--migrations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./migrations/ &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./schemas/

&lt;span class="c"&gt;# Preview without touching Contentful&lt;/span&gt;
npx cms-sim &lt;span class="nt"&gt;--schemas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./schemas/ &lt;span class="nt"&gt;--open&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if you are working with an existing Contentful space:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Pull the content model from your space (read-only, CDA token)&lt;/span&gt;
npx cms-sim pull &lt;span class="nt"&gt;--space-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_SPACE_ID &lt;span class="nt"&gt;--access-token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_CDA_TOKEN

&lt;span class="c"&gt;# Simulate locally against the pulled schemas&lt;/span&gt;
npx cms-sim &lt;span class="nt"&gt;--schemas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./pulled/schemas/ &lt;span class="nt"&gt;--open&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What is still missing
&lt;/h2&gt;

&lt;p&gt;A lot.&lt;/p&gt;

&lt;p&gt;This is still pre-1.0 work, and I do not want to fake maturity where it does not exist.&lt;/p&gt;

&lt;p&gt;There is still polish to do. There are still workflow edges to improve. There is still room to make the UX sharper, the docs tighter, and the tool more battle-ready for bigger real-world cases.&lt;/p&gt;

&lt;p&gt;That is exactly why I am sharing it now.&lt;/p&gt;

&lt;p&gt;I like build-in-public when it is honest. Not “look at my perfect tool,” but “here is the problem, here is the current state, here is the direction.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I am posting this now
&lt;/h2&gt;

&lt;p&gt;Because I think Contentful teams deserve better tooling around model design, migration preview, and validation.&lt;/p&gt;

&lt;p&gt;And because I want to contribute something useful to that space while I am still learning from the build itself.&lt;/p&gt;

&lt;p&gt;So this is where the project stands today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;current version:&lt;/strong&gt; &lt;code&gt;v0.3.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;status:&lt;/strong&gt; still in progress&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;goal:&lt;/strong&gt; &lt;code&gt;v1.0.0&lt;/code&gt; by the end of May&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you work with Contentful and this problem sounds familiar, feel free to take a look, break it, question it, or tell me where the current workflow still feels weak.&lt;/p&gt;

&lt;p&gt;That feedback is more useful to me than fake applause.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffokt15lgzi5wiykghkrx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffokt15lgzi5wiykghkrx.png" alt="content-model-simulator v0.3.0" width="800" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/JoshuaPozos/content-model-simulator" rel="noopener noreferrer"&gt;github.com/JoshuaPozos/content-model-simulator&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Package:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/content-model-simulator" rel="noopener noreferrer"&gt;npmjs.com/package/content-model-simulator&lt;/a&gt;&lt;/p&gt;

</description>
      <category>contentful</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>devtools</category>
    </item>
    <item>
      <title>GraphQL Server Using NEXT.js and MongoDB Atlas</title>
      <dc:creator>Joshua Pozos</dc:creator>
      <pubDate>Mon, 02 Aug 2021 23:41:17 +0000</pubDate>
      <link>https://dev.to/joshuapozos/graphql-server-using-next-js-and-mongodb-atlas-93i</link>
      <guid>https://dev.to/joshuapozos/graphql-server-using-next-js-and-mongodb-atlas-93i</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;First, sorry if something is not well written, I am still learning English&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Add the following dependencies in your project
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add apollo-server-micro@2.25.1 mongoose graphql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;I used this version of apollo-server-micro because the new version 3 sends you to a sandbox that in my opinion is uncomfortable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the root of your project create a folder with the name you prefer, in this case I use "db". Inside create two more folders, one called "config" and the other "models"&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir db &amp;amp;&amp;amp; cd db &amp;amp;&amp;amp; mkdir config &amp;amp;&amp;amp; mkdir models
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk3f2fvj66a3gb3qejnj6.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk3f2fvj66a3gb3qejnj6.jpeg" alt="Alt Text" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Config
&lt;/h3&gt;

&lt;p&gt;Inside the config folder create an index.js file and add "moongose". For the MongoDb uri it is good practice to add it inside an .env file and bring it with proccess.env.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const mongoose = require('mongoose')

const MongoDb = process.env.MONGODB_URI

const connectDb = async () =&amp;gt; {
  try {
    await mongoose.connect(MongoDb, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      useFindAndModify: false,
      useCreateIndex: true,
    })
    console.log('db success connect')
  } catch (err) {
    console.log('error connecting to database')
    console.log(err)
    process.exit(1)
  }
}

module.exports = connectDb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Models
&lt;/h3&gt;

&lt;p&gt;Now we are going to create an example model, in this case the product model. So inside the "models" folder I create a file called product.js and add moongose and what I need for the model.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import mongoose from 'mongoose'

const { Schema } = mongoose

mongoose.Promise = global.Promise

const ProductsSchema = new Schema({
  name: {
    type: String,
    required: true,
    trim: true,
  },
  productionCapacity: {
    type: Number,
    required: true,
    trim: true,
  },
  price: {
    type: Number,
    required: true,
    trim: true,
  },
  description: {
    type: String,
    trim: true,
  },
  createAt: {
    type: Date,
    defalut: Date.now(),
  },
})

ProductsSchema.index({ name: 'text' })

module.exports =
  mongoose.models.Product || mongoose.model('Product', ProductsSchema)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Schema and resolvers
&lt;/h3&gt;

&lt;p&gt;I'll leave the code here as an example because your project can be totally different.&lt;/p&gt;
&lt;h4&gt;
  
  
  Schema
&lt;/h4&gt;

&lt;p&gt;For the schema we will import &lt;code&gt;gql&lt;/code&gt; from &lt;code&gt;apollo-server-micro&lt;/code&gt; and create a constant called "typeDefs" in which we will use &lt;code&gt;gql&lt;/code&gt; and inside we will define our types, inputs, queries and mutations&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { gql } from 'apollo-server-micro'

const typeDefs = gql`
  # Products
  type Product {
    id: ID
    name: String
    productionCapacity: Int
    price: Float
    description: String
  }

  input ProductInput {
    name: String!
    productionCapacity: Int!
    price: Float!
    description: String
  }

  type Query {
    getProducts: [Product]
    getProduct(id: ID!): Product
  }

  type Mutation {
    #Products
    newProduct(input: ProductInput): Product
    updateProduct(id: ID!, input: ProductInput): Product
    deleteProduct(id: ID!): String
  }
`

module.exports = typeDefs

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  Resolvers
&lt;/h4&gt;

&lt;p&gt;I will leave the code that I use, but remember that your project can be very different. We bring the models, in this case I bring my "product" model to be able to use it within the queries or mutations that I define.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const Product = require('./models/product')

const resolvers = {
  Query: {
    // products
    getProducts: async () =&amp;gt; {
      try {
        const products = await Product.find({})

        return products
      } catch (err) {
        console.log(err)
      }
    },
    getProduct: async (_, { id }) =&amp;gt; {
      const product = await Product.findById(id)

      if (!product) {
        throw new Error('Product not found')
      }

      return product
    },
  },

  Mutation: {
    // products
    newProduct: async (_, { input }) =&amp;gt; {
      try {
        const product = new Product(input)

        const result = await product.save()

        return result
      } catch (err) {
        console.log(err)
      }
    },
    updateProduct: async (_, { id, input }) =&amp;gt; {
      let product = await Product.findById(id)

      if (!product) {
        throw new Error('Product not found')
      }

      product = await Product.findOneAndUpdate({ _id: id }, input, {
        new: true,
      })

      return product
    },
    deleteProduct: async (_, { id }) =&amp;gt; {
      const product = await Product.findById(id)

      if (!product) {
        throw new Error('Producto no encontrado')
      }

      await Product.findOneAndDelete({ _id: id })

      return 'Producto eliminado'
    },
  },
}

module.exports = resolvers

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Creating the endpoint
&lt;/h3&gt;

&lt;p&gt;First we create a graphql.js file (it can be called whatever) inside /pages/api.&lt;br&gt;
In the graphql.js file we are going to import &lt;code&gt;ApolloServer&lt;/code&gt; and &lt;code&gt;makeExecutableSchema&lt;/code&gt; from &lt;code&gt;apollo-server-micro&lt;/code&gt; and &lt;code&gt;typeDefs&lt;/code&gt;, &lt;code&gt;resolvers&lt;/code&gt; and &lt;code&gt;connectDb&lt;/code&gt; from their corresponding files. Then we run the &lt;code&gt;connectDb()&lt;/code&gt; function and create a schema with &lt;code&gt;makeExecutableSchema&lt;/code&gt; which will have the &lt;code&gt;typeDefs&lt;/code&gt; and the &lt;code&gt;resolvers&lt;/code&gt;.&lt;br&gt;
And at the end we export the Apollo Server passing it our schema variable and defining the path that must match the name of the file that we put in /pages/api.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { ApolloServer, makeExecutableSchema } from 'apollo-server-micro'
import typeDefs from '../../db/schema'
import resolvers from '../../db/resolvers'
import connectDb from '../../db/config'

connectDb()

export const schema = makeExecutableSchema({
  typeDefs,
  resolvers
})

export const config = {
  api: {
    bodyParser: false,
  },
}

export default new ApolloServer({ schema }).createHandler({
  path: '/api/graphql',
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Your project should have been more or less like this&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7vnhwkd6eojebi4ld149.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7vnhwkd6eojebi4ld149.jpeg" alt="Alt Text" width="280" height="528"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now run the project and go to the url of your api with graphql&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl3jccklkxbrobcf3dc46.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl3jccklkxbrobcf3dc46.jpeg" alt="Alt Text" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I leave the package.json for you to check the version of the dependencies used in this post.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "name": "graphql-nextjs",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "apollo-server-micro": "2.25.1",
    "graphql": "^15.5.1",
    "mongoose": "^5.13.5",
    "next": "11.0.1",
    "react": "17.0.2",
    "react-dom": "17.0.2"
  },
  "devDependencies": {
    "eslint": "7.32.0",
    "eslint-config-next": "11.0.1"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Repo
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/JoshuaPozos" rel="noopener noreferrer"&gt;
        JoshuaPozos
      &lt;/a&gt; / &lt;a href="https://github.com/JoshuaPozos/graphql-nextjs" rel="noopener noreferrer"&gt;
        graphql-nextjs
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;GraphQL Server Using NEXT.js and MongoDB Atlas&lt;/h1&gt;

&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Setup&lt;/h2&gt;

&lt;/div&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;yarn install
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Create ur own &lt;code&gt;MONGODB_URI&lt;/code&gt; into .env file (.env.development or .env.local) and add ur mongodb uri.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Ready&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;Modify all u need to ur project&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Development mode&lt;/h2&gt;

&lt;/div&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;yarn dev
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;

  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/JoshuaPozos/graphql-nextjs" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



</description>
      <category>nextjs</category>
      <category>graphql</category>
      <category>mongodb</category>
      <category>react</category>
    </item>
  </channel>
</rss>
