<?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: Tony Spiro</title>
    <description>The latest articles on DEV Community by Tony Spiro (@tonyspiro).</description>
    <link>https://dev.to/tonyspiro</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F36636%2F37a4c910-90d9-40b1-9a8b-69e1ad31b4f6.jpeg</url>
      <title>DEV Community: Tony Spiro</title>
      <link>https://dev.to/tonyspiro</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tonyspiro"/>
    <language>en</language>
    <item>
      <title>Best Headless CMS for Astro in 2026: 4 Options Compared</title>
      <dc:creator>Tony Spiro</dc:creator>
      <pubDate>Wed, 11 Nov 2026 17:01:56 +0000</pubDate>
      <link>https://dev.to/tonyspiro/best-headless-cms-for-astro-in-2026-4-options-compared-4p2i</link>
      <guid>https://dev.to/tonyspiro/best-headless-cms-for-astro-in-2026-4-options-compared-4p2i</guid>
      <description>&lt;p&gt;Astro builds static HTML by default, which changes what you should want from a CMS. The content fetch runs once at build time, not on every request, so raw API latency matters far less than it does on a server-rendered stack. What matters instead is whether the SDK works cleanly inside Astro frontmatter, whether &lt;code&gt;getStaticPaths&lt;/code&gt; can hydrate every page from a single query, and whether a rebuild gets triggered when an editor publishes.&lt;/p&gt;

&lt;p&gt;This guide compares four headless CMS platforms on those terms. Every price below was read directly from the vendor's own pricing page on November 10, 2026, and each figure is linked to its source. Vendors whose pricing could not be read and cited on that date were left out rather than guessed at.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Astro actually needs from a CMS
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A JavaScript SDK that runs in frontmatter.&lt;/strong&gt; Everything between the &lt;code&gt;---&lt;/code&gt; fences executes on the server at build time. An SDK that assumes a browser or a long-lived server process will fight you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lean queries.&lt;/strong&gt; A list page needs a title, a slug, and an image. Pulling full post bodies you never render is the easiest performance mistake to make on a static build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One query per route set.&lt;/strong&gt; &lt;code&gt;getStaticPaths&lt;/code&gt; runs once and enumerates every dynamic route. If you can pass the fetched record through &lt;code&gt;props&lt;/code&gt;, you build the whole blog in a single request instead of one lookup per page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A rebuild trigger.&lt;/strong&gt; Static output does not change when content changes. You need a webhook that calls your host's deploy hook on publish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Predictable empty-result behavior.&lt;/strong&gt; At build time, an unhandled error from the CMS fails the entire build, not one page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cosmic
&lt;/h2&gt;

&lt;p&gt;Cosmic is a SaaS headless CMS with a REST API and an official TypeScript SDK, &lt;code&gt;@cosmicjs/sdk&lt;/code&gt;. It is a Y Combinator company (W19). Astro's own documentation ships a &lt;a href="https://docs.astro.build/en/guides/cms/cosmic/" rel="noopener noreferrer"&gt;Cosmic integration guide&lt;/a&gt;, so the setup path is documented on both sides.&lt;/p&gt;

&lt;p&gt;The SDK is a plain async client, which is what you want inside frontmatter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/lib/cosmic.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;createBucketClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'@cosmicjs/sdk'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;cosmic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createBucketClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;bucketSlug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;COSMIC_BUCKET_SLUG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;readKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;COSMIC_READ_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Astro reads environment variables through &lt;code&gt;import.meta.env&lt;/code&gt;, not &lt;code&gt;process.env&lt;/code&gt;. That is the single most common porting error when you move example code over from a Next.js tutorial. Skip the &lt;code&gt;PUBLIC_&lt;/code&gt; prefix on these two values so they stay out of the browser bundle.&lt;/p&gt;

&lt;p&gt;Building every post page from one query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
// src/pages/blog/[slug].astro
import { cosmic } from '../../lib/cosmic';

export async function getStaticPaths() {
  const { objects } = await cosmic.objects
    .find({ type: 'blog-posts' })
    .props(['title', 'slug', 'metadata'])
    .depth(1);

  return objects.map((post: any) =&amp;gt; ({
    params: { slug: post.slug },
    props: { post },
  }));
}

const { post } = Astro.props;
---

&amp;lt;article&amp;gt;
  &amp;lt;h1&amp;gt;{post.title}&amp;lt;/h1&amp;gt;
  &amp;lt;div set:html={post.metadata.content} /&amp;gt;
&amp;lt;/article&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;getStaticPaths&lt;/code&gt; already fetched every post, handing the record through &lt;code&gt;props&lt;/code&gt; means no second lookup per page. &lt;code&gt;.depth(1)&lt;/code&gt; resolves relationships, so an author reference comes back as the full author object rather than an ID.&lt;/p&gt;

&lt;p&gt;One behavior worth knowing before your first build: when a query matches nothing, the SDK throws a 404 rather than returning an empty array, and both &lt;code&gt;find()&lt;/code&gt; and &lt;code&gt;findOne()&lt;/code&gt; do this. On a static build that fails the whole build, not one page. Wrap calls that can legitimately come back empty in a &lt;code&gt;try/catch&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;, read from the &lt;a href="https://www.cosmicjs.com/pricing?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=best-headless-cms-for-astro" rel="noopener noreferrer"&gt;Cosmic pricing page&lt;/a&gt; on November 10, 2026: Free is $0 with 1 Bucket, 2 team members and 1,000 Objects. Builder is $99/month with 2 Buckets, 3 team members and 5,000 Objects. Team is $299/month with 3 Buckets, 5 team members and 20,000 Objects. Business is $499/month with 5 Buckets, 10 team members and 50,000 Objects. Enterprise is custom. Additional team members are $29/user/month. Webhooks, localization, revision history and automatic backups are per-project add-ons at $99/month each, or $199/month for all four bundled.&lt;/p&gt;

&lt;p&gt;Be deliberate about that last line if you are costing out an Astro project: the publish-triggers-rebuild workflow depends on webhooks, so price the add-on in from the start rather than discovering it later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good fit if:&lt;/strong&gt; you want a hosted CMS with a typed SDK, an editor UI non-technical teammates can use without training, and every price published in full.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Think twice if:&lt;/strong&gt; you need to self-host or keep content on your own infrastructure. Cosmic is SaaS only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sanity
&lt;/h2&gt;

&lt;p&gt;Sanity is the most configurable option here. Content is queried with GROQ, its own query language, and the editing environment (Sanity Studio) is a React application you deploy and maintain yourself. If you have strong opinions about how your editing UI should behave, Sanity will let you build exactly that.&lt;/p&gt;

&lt;p&gt;For Astro specifically, the tradeoff is front-loaded work. Standing up Studio, writing your schemas, and learning GROQ is a development project before you render your first page. Teams with frontend engineering capacity get real value from it. Teams who want to ship a content site this week usually do not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;, read from the &lt;a href="https://www.sanity.io/pricing" rel="noopener noreferrer"&gt;Sanity pricing page&lt;/a&gt; on November 10, 2026: Free is $0 and includes up to 20 seats, 2 public datasets and 10,000 documents. Growth is $15 per seat/month, up to 50 seats, with 25,000 documents. Enterprise is custom.&lt;/p&gt;

&lt;p&gt;The free tier is unusually generous on seats, which makes Sanity cheap to evaluate. Model your costs on documents and datasets rather than seats, since that is where the ceilings bite first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storyblok
&lt;/h2&gt;

&lt;p&gt;Storyblok's distinguishing feature is its visual editor. Editors click a component on a live preview of the page and edit it in place. For a marketing site where layout changes constantly, that shortens the feedback loop a lot.&lt;/p&gt;

&lt;p&gt;The component-based model shapes how you structure content, which suits page building well and application-style content less well. On Astro, the visual editor needs a preview route your editors can load, so budget setup time for it rather than assuming it works against a static build out of the box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;, read from the &lt;a href="https://www.storyblok.com/pricing" rel="noopener noreferrer"&gt;Storyblok pricing page&lt;/a&gt; on November 10, 2026: Starter is free with 1 space and up to 2 seats. Growth is $99/month. Growth Plus is $349/month. Premium and Elite are custom. Additional seats on Starter and Growth are $15/month each, and annual billing is discounted below the monthly list price.&lt;/p&gt;

&lt;p&gt;The detail to catch is that Storyblok prices per space. If you run several distinct sites, that is several subscriptions, and the comparison against per-project competitors changes shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strapi
&lt;/h2&gt;

&lt;p&gt;Strapi is the open-source option. You can self-host it, read and modify the codebase, and keep your data on your own infrastructure. For teams with data residency or compliance requirements, that is a decisive advantage no SaaS platform can match.&lt;/p&gt;

&lt;p&gt;The cost is operational. You own deployments, version upgrades, security patches, and the database. Major-version upgrades have historically needed real migration work. Strapi Cloud removes most of that burden and adds a bill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;, read on November 10, 2026: on the &lt;a href="https://strapi.io/pricing-cloud" rel="noopener noreferrer"&gt;Strapi Cloud pricing page&lt;/a&gt;, Starter is $35 per project/month, Pro is $90 per project/month, and Business is $450 per project/month. On the &lt;a href="https://strapi.io/pricing-cms" rel="noopener noreferrer"&gt;Strapi CMS pricing page&lt;/a&gt;, the self-hosted Community edition is free and open source, with paid self-hosted editions quoted on the same page.&lt;/p&gt;

&lt;p&gt;Note that Cloud is priced per project, so a multi-site Astro setup multiplies that figure rather than sharing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You want to ship an Astro site quickly, with a typed SDK and an editor UI your marketing team can use:&lt;/strong&gt; Cosmic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You have frontend engineers and want to shape the editing experience yourself:&lt;/strong&gt; Sanity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your editors need to click a live page and edit it in place:&lt;/strong&gt; Storyblok.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You must self-host, own the data, or meet residency requirements:&lt;/strong&gt; Strapi.&lt;/p&gt;

&lt;p&gt;For most Astro projects the deciding factor is not raw API capability, since all four will serve content to a build step perfectly well. It is how much setup work sits between &lt;code&gt;npm create astro@latest&lt;/code&gt; and an editor publishing without filing a ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build it
&lt;/h2&gt;

&lt;p&gt;The fastest way to judge any of these is to put a real site on it. The &lt;a href="https://www.cosmicjs.com/learn/ship-a-site-on-cosmic-with-astro?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=best-headless-cms-for-astro" rel="noopener noreferrer"&gt;Astro lesson in Cosmic Learn&lt;/a&gt; walks the whole path: SDK install, lean queries with &lt;code&gt;.props()&lt;/code&gt;, static routes with &lt;code&gt;getStaticPaths&lt;/code&gt;, and a Vercel deploy with the rebuild webhook wired up. The &lt;a href="https://www.cosmicjs.com/headless-cms-for-astro?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=best-headless-cms-for-astro" rel="noopener noreferrer"&gt;Astro CMS overview&lt;/a&gt; covers content modeling and the editor experience on one page, and the &lt;a href="https://www.cosmicjs.com/docs?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=best-headless-cms-for-astro" rel="noopener noreferrer"&gt;API documentation&lt;/a&gt; has the full query reference.&lt;/p&gt;

&lt;p&gt;If you want the wider field rather than the Astro-specific one, we compare ten platforms in &lt;a href="https://www.cosmicjs.com/blog/best-headless-cms-2026?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=best-headless-cms-for-astro" rel="noopener noreferrer"&gt;Best Headless CMS in 2026&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://app.cosmicjs.com/signup?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=best-headless-cms-for-astro" rel="noopener noreferrer"&gt;Start free with Cosmic&lt;/a&gt;&lt;/strong&gt;, no credit card required. If you would rather talk through a migration or a multi-site setup first, &lt;a href="https://calendly.com/tonyspiro/cosmic-intro?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=best-headless-cms-for-astro" rel="noopener noreferrer"&gt;book time with our CEO&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>cms</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Best Headless CMS for Nuxt in 2026: 7 Options Compared</title>
      <dc:creator>Tony Spiro</dc:creator>
      <pubDate>Tue, 10 Nov 2026 17:19:58 +0000</pubDate>
      <link>https://dev.to/tonyspiro/best-headless-cms-for-nuxt-in-2026-7-options-compared-gj0</link>
      <guid>https://dev.to/tonyspiro/best-headless-cms-for-nuxt-in-2026-7-options-compared-gj0</guid>
      <description>&lt;p&gt;Nuxt gives you server routes, &lt;code&gt;useAsyncData&lt;/code&gt;, Nitro caching, and hybrid rendering out of the box. The CMS you put behind it decides three things: how fast your editors can publish without filing a ticket, how much infrastructure your team ends up owning, and what the bill looks like when the second and third site show up.&lt;/p&gt;

&lt;p&gt;This is a comparison of seven options that work well with Nuxt 3 and Nuxt 4. I fetched every vendor's pricing page on &lt;strong&gt;November 10, 2026&lt;/strong&gt; and linked each figure to the page it came from, with the date. Pricing moves. When you read this, check the links.&lt;/p&gt;

&lt;p&gt;A note on what is here: a vendor only made the list if its current pricing page could be fetched and cited. No remembered numbers, no "approximately," no vendor quoted from an older version of this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CMS&lt;/th&gt;
&lt;th&gt;Hosting model&lt;/th&gt;
&lt;th&gt;Content lives in&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cosmic&lt;/td&gt;
&lt;td&gt;Managed SaaS&lt;/td&gt;
&lt;td&gt;Hosted API&lt;/td&gt;
&lt;td&gt;Teams running several sites or brands who want editors publishing without engineering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nuxt Content&lt;/td&gt;
&lt;td&gt;Self-hosted, in your repo&lt;/td&gt;
&lt;td&gt;Markdown/YAML/JSON files in git&lt;/td&gt;
&lt;td&gt;Docs, blogs, and marketing sites where the authors are developers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storyblok&lt;/td&gt;
&lt;td&gt;Managed SaaS&lt;/td&gt;
&lt;td&gt;Hosted API&lt;/td&gt;
&lt;td&gt;Marketing teams who want visual, block-level page editing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sanity&lt;/td&gt;
&lt;td&gt;Managed SaaS&lt;/td&gt;
&lt;td&gt;Hosted API&lt;/td&gt;
&lt;td&gt;Teams who want a fully customizable editing environment and will build it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strapi&lt;/td&gt;
&lt;td&gt;Self-hosted or Strapi Cloud&lt;/td&gt;
&lt;td&gt;Your own database&lt;/td&gt;
&lt;td&gt;Teams who need the content layer inside their own infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prismic&lt;/td&gt;
&lt;td&gt;Managed SaaS&lt;/td&gt;
&lt;td&gt;Hosted API&lt;/td&gt;
&lt;td&gt;Small sites and single-repository projects with a tight budget&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Directus&lt;/td&gt;
&lt;td&gt;Self-hosted or Directus Cloud&lt;/td&gt;
&lt;td&gt;Your own SQL database&lt;/td&gt;
&lt;td&gt;Teams wrapping an existing database with an admin and API layer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. Cosmic
&lt;/h2&gt;

&lt;p&gt;Cosmic is a managed headless CMS. You model content in a dashboard, editors publish there, and Nuxt reads it over a REST API or the TypeScript SDK. There is nothing to deploy, patch, or scale on your side. Cosmic is a Y Combinator W19 company.&lt;/p&gt;

&lt;p&gt;For Nuxt specifically, the fit is straightforward. Content comes back as plain JSON from an edge-cached API, so it drops into a Nitro server route or &lt;code&gt;useAsyncData&lt;/code&gt; with no adapter layer in between. Image optimization and video delivery are handled on the CDN, so you are not wiring up a separate transform service for your &lt;code&gt;&amp;lt;NuxtImg&amp;gt;&lt;/code&gt; sources.&lt;/p&gt;

&lt;p&gt;Where Cosmic separates from the rest of this list is the multi-property case. If you run five brand sites, or a franchise network, or a set of client sites, you can keep a Bucket per brand while sharing content models and billing across them, instead of standing up a separate install or a separate space per site.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt; (&lt;a href="https://www.cosmicjs.com/pricing" rel="noopener noreferrer"&gt;cosmicjs.com/pricing&lt;/a&gt;, read November 10, 2026): Free at &lt;strong&gt;$0/month&lt;/strong&gt; with 1 Bucket, 2 team members, and 1,000 Objects. &lt;strong&gt;Builder $99/month&lt;/strong&gt;, &lt;strong&gt;Team $299/month&lt;/strong&gt;, &lt;strong&gt;Business $499/month&lt;/strong&gt;, and Enterprise at custom pricing. Additional users beyond a plan's included team members are &lt;strong&gt;$29/user/month&lt;/strong&gt;. Per-project feature add-ons (Webhooks, Localization, Revision History, Automatic Backups) are $99/month each or $199/month for all four.&lt;/p&gt;

&lt;p&gt;Cosmic charges per seat once you pass your plan's included team members, so price it on the real size of your editorial team rather than the base plan alone.&lt;/p&gt;

&lt;p&gt;The editorial argument, from a real customer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Cosmic is: us never having to ask a developer to change anything on the backend of our website."&lt;br&gt;
— Maximilian Wuhr, Co-Founder at FINN&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Strengths:&lt;/strong&gt; No infrastructure to run. Editors ship independently. Clean multi-site and multi-locale story. REST API, TypeScript SDK, and a CLI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt; It is a hosted service, so self-hosting is not an option. The free tier's 1,000 Objects is fine for a marketing site and tight for a large content archive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading Cosmic content in Nuxt
&lt;/h3&gt;

&lt;p&gt;Install the official SDK, &lt;code&gt;@cosmicjs/sdk&lt;/code&gt;, then create a server route so your read key never reaches the browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/api/posts.get.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;createBucketClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'@cosmicjs/sdk'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;cosmic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createBucketClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;bucketSlug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;COSMIC_BUCKET_SLUG&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;readKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;COSMIC_READ_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineEventHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;objects&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cosmic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'posts'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;props&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'slug'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'metadata'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;objects&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then consume it in a page with Nuxt's own data layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- pages/blog/index.vue --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;useAsyncData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'posts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/api/posts'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;v-for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"post in posts"&lt;/span&gt; &lt;span class="na"&gt;:key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"post.id"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;NuxtLink&lt;/span&gt; &lt;span class="na"&gt;:to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"`/blog/${post.slug}`"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/NuxtLink&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole integration. No adapter package, no build step, no schema generation. Full method reference is in the &lt;a href="https://www.cosmicjs.com/docs?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=best-headless-cms-for-nuxt" rel="noopener noreferrer"&gt;Cosmic docs&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Running more than one Nuxt site?&lt;/strong&gt; That is where a managed CMS stops being a convenience and starts being the thing that keeps your team off the critical path. &lt;a href="https://www.cosmicjs.com/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=best-headless-cms-for-nuxt" rel="noopener noreferrer"&gt;Start free on Cosmic&lt;/a&gt; and model your first content type in a few minutes.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Nuxt Content
&lt;/h2&gt;

&lt;p&gt;Nuxt Content is the first-party option, and for a certain kind of project it is the correct answer before you evaluate anything else. Content lives as Markdown, YAML, CSV, or JSON files inside your repository, and you query it with a MongoDB-like query builder backed by SQLite (&lt;a href="https://content.nuxt.com/" rel="noopener noreferrer"&gt;content.nuxt.com&lt;/a&gt;, read November 10, 2026).&lt;/p&gt;

&lt;p&gt;The feature that makes it distinctly Nuxt is MDC syntax: you use Vue components, with props and slots, directly inside Markdown files. Your content authors get a hero block or a callout component without you building a block renderer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// content.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;defineContentConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;defineCollection&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'@nuxt/content'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'zod'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineContentConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;blog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;defineCollection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'blog/*.md'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'page'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Collections give you schema validation and type-safety on frontmatter, with autocompletion in your Vue files. Querying is &lt;code&gt;queryCollection('blog').all()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For non-developer editing, Nuxt Studio is the visual layer. Per content.nuxt.com (read November 10, 2026), Studio is described as a free and open-source visual interface for editing content in production, with live preview on the production site, visual editing of Markdown, YML, and JSON files, and publishing changes directly to your git provider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing:&lt;/strong&gt; Nuxt Content is an open-source Nuxt module. Your cost is your hosting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strengths:&lt;/strong&gt; Zero external service. Content versioned in git alongside code. Vue components inside Markdown. Type-safe queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt; Content is coupled to deploys and to your repository. Editors need git as the system of record, which suits documentation sites and suits a 12-person marketing team considerably less. There is no hosted media pipeline, roles and permissions layer, or multi-brand content model to inherit.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Storyblok
&lt;/h2&gt;

&lt;p&gt;Storyblok is a managed CMS built around a visual editor, where editors compose pages from reusable blocks and see the result rendered live. Its Vue and Nuxt support has been a focus for years, which is why it shows up in nearly every Nuxt CMS shortlist.&lt;/p&gt;

&lt;p&gt;If your buyers are marketers who want to build landing pages without a developer, the block-based visual editing model is the main reason to pick it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt; (&lt;a href="https://www.storyblok.com/pricing" rel="noopener noreferrer"&gt;storyblok.com/pricing&lt;/a&gt;, read November 10, 2026): a free &lt;strong&gt;Starter&lt;/strong&gt; tier, &lt;strong&gt;Growth at $99/month&lt;/strong&gt; (lower on annual billing), &lt;strong&gt;Growth Plus at $349/month&lt;/strong&gt;, and Enterprise at custom pricing. Seats are limited per tier, with additional seats billed per seat on top of the plan price, so check the current seat count on the plan card against your editorial headcount.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strengths:&lt;/strong&gt; Strong visual page building. Long-standing Vue and Nuxt focus. Component-shaped content models that map cleanly to Vue components.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt; The block model is opinionated. Content that is not page-shaped, like a product catalog or a structured data set, fits it less naturally.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Sanity
&lt;/h2&gt;

&lt;p&gt;Sanity pairs a hosted content backend with Studio, an editing environment you configure in code and deploy yourself. If you want the authoring experience to be a first-class part of your codebase, that is the draw. It also means the editor UI is a project you own and maintain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt; (&lt;a href="https://www.sanity.io/pricing" rel="noopener noreferrer"&gt;sanity.io/pricing&lt;/a&gt;, read November 10, 2026): a &lt;strong&gt;Free&lt;/strong&gt; tier at $0 with generous seat allowance and capped documents, datasets, and API requests. &lt;strong&gt;Growth is billed per seat at $15/seat/month&lt;/strong&gt;, with document and dataset caps and paid add-ons for additional quota and support. Enterprise is custom.&lt;/p&gt;

&lt;p&gt;Watch the add-on line items rather than the headline seat price. On the plan page read November 10, 2026, extra datasets and increased quota are separately priced and can dominate the bill well before seat count does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strengths:&lt;/strong&gt; Highly customizable editing environment. Real-time collaboration. Strong structured content modeling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt; You are building and maintaining the Studio. Usage-based caps on documents, datasets, and API requests mean cost scales with what your site actually does, which takes modeling before you commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Strapi
&lt;/h2&gt;

&lt;p&gt;Strapi is the default answer when the requirement is "the content layer runs in our infrastructure." It is a Node.js CMS with an admin panel, running against your own database. For a Nuxt team already deploying Node services, there is nothing unfamiliar about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing, self-hosted&lt;/strong&gt; (&lt;a href="https://strapi.io/pricing-cms" rel="noopener noreferrer"&gt;strapi.io/pricing-cms&lt;/a&gt;, read November 10, 2026): a free &lt;strong&gt;Community&lt;/strong&gt; edition under the MIT license. &lt;strong&gt;Growth at $45/month&lt;/strong&gt; including 3 users, with additional users at $15/user/month, an SSO add-on priced separately, and Enterprise at custom pricing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing, Strapi Cloud&lt;/strong&gt; (&lt;a href="https://strapi.io/pricing-cloud" rel="noopener noreferrer"&gt;strapi.io/pricing-cloud&lt;/a&gt;, read November 10, 2026): &lt;strong&gt;Starter at $35/project/month&lt;/strong&gt;, &lt;strong&gt;Pro at $90/project/month&lt;/strong&gt;, and &lt;strong&gt;Business at $450/project/month&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The per-project structure on Cloud is the number to model carefully if you run several sites. Five properties on Pro is five times $90/month, before seats.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strengths:&lt;/strong&gt; Open source and self-hostable. Your data in your database. Extensible via plugins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt; Self-hosting means you own upgrades, scaling, backups, and uptime. Cloud pricing is per project, which gets expensive across a portfolio of sites.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Prismic
&lt;/h2&gt;

&lt;p&gt;Prismic is a managed CMS organized around a repository per project, with a page-builder style authoring experience. Its appeal is the price of entry for small projects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt; (&lt;a href="https://prismic.io/pricing" rel="noopener noreferrer"&gt;prismic.io/pricing&lt;/a&gt;, read November 10, 2026): &lt;strong&gt;Free at $0&lt;/strong&gt; with a single user, then &lt;strong&gt;Starter at $10/month&lt;/strong&gt;, &lt;strong&gt;Small at $25/month&lt;/strong&gt;, &lt;strong&gt;Medium at $150/month&lt;/strong&gt;, and &lt;strong&gt;Platinum at $675/month&lt;/strong&gt;, with Enterprise custom. Plans are priced per repository, and user counts and locale counts step up with each tier.&lt;/p&gt;

&lt;p&gt;The per-repository structure is the thing to check. One site on Small is inexpensive. Eight sites on Small is eight subscriptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strengths:&lt;/strong&gt; Low entry price. Page-builder authoring that non-developers pick up quickly. Solid Nuxt community support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt; Per-repository pricing scales poorly across a portfolio. Locale allowances are tier-gated, which matters if you are international early.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Directus
&lt;/h2&gt;

&lt;p&gt;Directus wraps an existing SQL database with an admin interface and an API, which makes it a strong pick when the data already exists and you want a content layer over it rather than a new content store.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt; (&lt;a href="https://directus.com/pricing" rel="noopener noreferrer"&gt;directus.com/pricing&lt;/a&gt;, read November 10, 2026): &lt;strong&gt;Core at $89/month billed yearly, or $99 monthly&lt;/strong&gt;, including 3 user seats, 25 collections, and 5 flows. Additional seats are listed at $50/seat/month and additional collections at $100 per 25-pack. There is also an &lt;strong&gt;Open Innovation Grant&lt;/strong&gt;: the page states that organizations under $5M in annual revenue with fewer than 50 employees qualify for fully permissive access at no software cost when self-hosted, with cloud hosting at $99/month.&lt;/p&gt;

&lt;p&gt;One caveat worth flagging, because it affects budgeting. On the page as read on November 10, 2026, the Team tier is stated two different ways on the same page: the plan card shows &lt;strong&gt;$588/month billed yearly, or $698 monthly&lt;/strong&gt;, while the FAQ further down states &lt;strong&gt;$499/month on an annual commitment ($5,988/year), or $599/month monthly&lt;/strong&gt;. Both were live when I read it. Confirm the current figure with Directus before you plan around it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strengths:&lt;/strong&gt; Works against your existing database schema. Self-hostable. Flows for automation. Granular access control on paid tiers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt; It is a data platform first and a publishing tool second, so the editorial experience needs more shaping for a marketing team. Seat and collection limits on Core are tight, and the next tier is a significant step up.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Your authors are developers and the site is a blog or docs.&lt;/strong&gt; Use Nuxt Content. It is first-party, free, and MDC is genuinely better than a block renderer you would build yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You have non-technical editors and one site.&lt;/strong&gt; Storyblok or Prismic. Both give editors a visual surface, and Prismic's entry tiers are cheap for a single repository.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You want the editing environment to be code you own.&lt;/strong&gt; Sanity, with the understanding that Studio becomes a project on your roadmap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Regulatory or infrastructure requirements say the content layer stays in-house.&lt;/strong&gt; Strapi self-hosted, or Directus if you are putting a content layer over a database that already exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You run several sites, brands, or client properties and editors keep waiting on engineering.&lt;/strong&gt; Cosmic. The per-property math is where this gets decided: per-project and per-repository pricing models compound fast across a portfolio, and running your own installs multiplies the upgrade and uptime work by the number of sites.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does Cosmic work with Nuxt 3 and Nuxt 4?&lt;/strong&gt;&lt;br&gt;
Yes. Cosmic is framework-agnostic. You call the REST API or the TypeScript SDK from a Nitro server route or inside &lt;code&gt;useAsyncData&lt;/code&gt;, exactly as in the example above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Cosmic offer GraphQL?&lt;/strong&gt;&lt;br&gt;
No. Cosmic provides a REST API, the TypeScript SDK (&lt;code&gt;@cosmicjs/sdk&lt;/code&gt;), and a CLI. If a GraphQL layer is a hard requirement for your stack, that is a real consideration to weigh.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I preview drafts in Nuxt?&lt;/strong&gt;&lt;br&gt;
Yes. Fetch with a preview-enabled client in a server route guarded by a preview cookie or token, then render the draft on the real site.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much does Cosmic cost for a small team?&lt;/strong&gt;&lt;br&gt;
Free at $0/month covers 1 Bucket, 2 team members, and 1,000 Objects. Builder is $99/month, Team is $299/month, Business is $499/month, and additional users beyond your plan's included team members are $29/user/month (&lt;a href="https://www.cosmicjs.com/pricing" rel="noopener noreferrer"&gt;cosmicjs.com/pricing&lt;/a&gt;, read November 10, 2026).&lt;/p&gt;

&lt;h2&gt;
  
  
  Start building
&lt;/h2&gt;

&lt;p&gt;Pick the one that matches who edits your content and how many properties you run. Those two questions decide this more reliably than a feature matrix does.&lt;/p&gt;

&lt;p&gt;If you are running Nuxt and you want editors publishing on their own schedule across every site you own, &lt;a href="https://www.cosmicjs.com/pricing?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=best-headless-cms-for-nuxt" rel="noopener noreferrer"&gt;see Cosmic plans and start free&lt;/a&gt;. Free tier, no credit card.&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>vue</category>
      <category>cms</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Best Sanity Alternatives in 2026: 5 Compared</title>
      <dc:creator>Tony Spiro</dc:creator>
      <pubDate>Tue, 10 Nov 2026 17:13:57 +0000</pubDate>
      <link>https://dev.to/tonyspiro/best-sanity-alternatives-in-2026-5-compared-eln</link>
      <guid>https://dev.to/tonyspiro/best-sanity-alternatives-in-2026-5-compared-eln</guid>
      <description>&lt;p&gt;Sanity is one of the most capable headless CMSs available: a fully customizable editing app, real-time collaboration, and a flexible query language. Teams usually look for an alternative for one of four reasons: per-seat pricing as the content team grows, an editing interface that needs a developer for every change, GROQ as one more language to learn, or wanting visual page building out of the box.&lt;/p&gt;

&lt;p&gt;This guide covers five alternatives. Every price and plan limit below was read directly from the vendor's own pricing page on November 10, 2026, and each one links to the page it came from. We make Cosmic, so weigh our take on it accordingly. Platforms are listed alphabetically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sanity's pricing, for reference
&lt;/h2&gt;

&lt;p&gt;Sanity's Free plan includes 20 seats, 2 public datasets, and 10,000 documents. Growth is $15 per seat per month, so a 20-person team on Growth pays $300/month before any usage overages. Enterprise is priced on request. Source: &lt;a href="https://www.sanity.io/pricing" rel="noopener noreferrer"&gt;sanity.io/pricing&lt;/a&gt;, read November 10, 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CMS&lt;/th&gt;
&lt;th&gt;Free tier&lt;/th&gt;
&lt;th&gt;Paid plans from&lt;/th&gt;
&lt;th&gt;Source, read Nov 10 2026&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cosmic&lt;/td&gt;
&lt;td&gt;$0: 1 Bucket, 2 team members, 1,000 Objects&lt;/td&gt;
&lt;td&gt;$99/month (Builder)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.cosmicjs.com/pricing" rel="noopener noreferrer"&gt;cosmicjs.com/pricing&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hygraph&lt;/td&gt;
&lt;td&gt;Hobby $0: 3 seats, 1,000 entries, 2 locales&lt;/td&gt;
&lt;td&gt;From $199/month (Growth)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://hygraph.com/pricing" rel="noopener noreferrer"&gt;hygraph.com/pricing&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prismic&lt;/td&gt;
&lt;td&gt;Free: 1 user, 4M API calls/month, unlimited documents&lt;/td&gt;
&lt;td&gt;$10/month (Starter, per repository, paid annually)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://prismic.io/pricing" rel="noopener noreferrer"&gt;prismic.io/pricing&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storyblok&lt;/td&gt;
&lt;td&gt;Starter: free, 1 seat&lt;/td&gt;
&lt;td&gt;$99/month (Growth, 5 seats)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.storyblok.com/pricing" rel="noopener noreferrer"&gt;storyblok.com/pricing&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strapi&lt;/td&gt;
&lt;td&gt;No free tier listed on Strapi Cloud&lt;/td&gt;
&lt;td&gt;Cloud from $35/project/month (Starter)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://strapi.io/pricing-cloud" rel="noopener noreferrer"&gt;strapi.io/pricing-cloud&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sanity (incumbent)&lt;/td&gt;
&lt;td&gt;Free: 20 seats, 2 public datasets, 10,000 documents&lt;/td&gt;
&lt;td&gt;$15/seat/month (Growth)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.sanity.io/pricing" rel="noopener noreferrer"&gt;sanity.io/pricing&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What to check before switching
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Portable Text.&lt;/strong&gt; Sanity stores rich text as Portable Text, a JSON structure. Alternatives use a different format (HTML, Markdown, or their own JSON), so plan a conversion step and test how embedded objects and custom marks map over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Customization you rely on.&lt;/strong&gt; If your Studio has custom input components, document actions, or workflows, list them first. Hosted alternatives configured in a web interface trade that flexibility for not having to maintain an editor codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References and queries.&lt;/strong&gt; GROQ queries that join documents need rewriting against the new API. Check depth limits on references and how the new API returns nested content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real-time collaboration.&lt;/strong&gt; Sanity's simultaneous editing is a genuine strength. Most alternatives offer drafts and versions with less real-time co-editing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The alternatives
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cosmic
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Versus Sanity:&lt;/strong&gt; content models are built in the dashboard rather than in code, so editors and non-developers can add fields. Plans include a set number of team members rather than charging purely per seat, with additional members at $29 each per month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing:&lt;/strong&gt; Free is $0 with 1 Bucket, 2 team members, and 1,000 Objects. Builder is $99/month, Team $299/month, Business $499/month, additional users $29/month. Source: &lt;a href="https://www.cosmicjs.com/pricing" rel="noopener noreferrer"&gt;cosmicjs.com/pricing&lt;/a&gt;, read November 10, 2026.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch for:&lt;/strong&gt; less editor customization than Sanity Studio, no real-time co-editing on the same document, and a smaller community.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Hygraph
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Versus Sanity:&lt;/strong&gt; content federation. The Growth plan includes one remote source for unifying external APIs alongside your content, and Enterprise goes up to ten. Source: &lt;a href="https://hygraph.com/pricing" rel="noopener noreferrer"&gt;hygraph.com/pricing&lt;/a&gt;, read November 10, 2026.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing:&lt;/strong&gt; Hobby is free with 3 seats, 1,000 entries, 2 locales, and 500,000 API calls. Growth is from $199/month with 10 seats, 10,000 entries, and 3 locales. Enterprise is custom. Same source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch for:&lt;/strong&gt; the step from free to $199/month, and SSO sitting on Enterprise only.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Prismic
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Versus Sanity:&lt;/strong&gt; editors assemble pages from developer-defined slices in a Visual Page Builder, with Slice Machine on the developer side. Source: &lt;a href="https://prismic.io/pricing" rel="noopener noreferrer"&gt;prismic.io/pricing&lt;/a&gt;, read November 10, 2026.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing:&lt;/strong&gt; Free includes 1 user, 4M API calls a month, and unlimited documents. Paid plans are per repository, paid annually: Starter $10/month (3 users), Small $25/month (7 users), Medium $150/month (25 users), Platinum $675/month (unlimited users). Same source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch for:&lt;/strong&gt; pricing is per repository, so several sites means several subscriptions. Locales are capped per plan (2 on Free, 8 on Platinum).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Storyblok
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Versus Sanity:&lt;/strong&gt; a visual editing workflow where marketers build pages from components.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing:&lt;/strong&gt; Starter is free with 1 seat. Growth is $99/month with 5 seats, Growth Plus $349/month with 15 seats. Source: &lt;a href="https://www.storyblok.com/pricing" rel="noopener noreferrer"&gt;storyblok.com/pricing&lt;/a&gt;, read November 10, 2026.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch for:&lt;/strong&gt; seat limits per plan. Larger teams move to Growth Plus or a custom tier.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Strapi
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Versus Sanity:&lt;/strong&gt; open source and self-hosted, with content types built in an admin UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing:&lt;/strong&gt; Strapi Cloud starts at $35 per project per month (Starter), with Pro at $90 and Business at $450. The Cloud pricing page lists no free Cloud tier, and its FAQ covers migrating from the self-hosted Community and Enterprise editions. Source: &lt;a href="https://strapi.io/pricing-cloud" rel="noopener noreferrer"&gt;strapi.io/pricing-cloud&lt;/a&gt;, read November 10, 2026.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch for:&lt;/strong&gt; Cloud bills per project, so costs scale with the number of sites. Self-hosting means running the server, database, and upgrades.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your editors want to build pages visually:&lt;/strong&gt; Storyblok or Prismic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need to federate content from external APIs:&lt;/strong&gt; Hygraph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want open source with a UI-built schema:&lt;/strong&gt; Strapi.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want plan-based pricing with a set number of included seats:&lt;/strong&gt; Cosmic.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Moving from Sanity to Cosmic
&lt;/h2&gt;

&lt;p&gt;See the &lt;a href="https://www.cosmicjs.com/sanity-alternative?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=sanity-alternatives" rel="noopener noreferrer"&gt;Sanity alternative overview&lt;/a&gt; for a side-by-side comparison.&lt;/p&gt;

&lt;p&gt;Cosmic's Free plan is $0 with 1 Bucket, 2 team members, and 1,000 Objects, so you can model your Sanity schema and test an import before paying anything. &lt;a href="https://www.cosmicjs.com/pricing?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=sanity-alternatives" rel="noopener noreferrer"&gt;See plans and start free&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>cms</category>
      <category>headlesscms</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Best Headless CMS to Replace WordPress in 2026</title>
      <dc:creator>Tony Spiro</dc:creator>
      <pubDate>Wed, 04 Nov 2026 15:40:33 +0000</pubDate>
      <link>https://dev.to/tonyspiro/best-headless-cms-to-replace-wordpress-in-2026-4kd6</link>
      <guid>https://dev.to/tonyspiro/best-headless-cms-to-replace-wordpress-in-2026-4kd6</guid>
      <description>&lt;p&gt;Teams leave WordPress for a headless CMS for familiar reasons: plugin updates and security patches, slow page builders, a theme that ties content to one site, and a frontend team that would rather work in Next.js, Astro, or Nuxt. The hard part is not picking a new CMS. It is moving years of posts, pages, media, and custom fields without breaking URLs or losing editors along the way.&lt;/p&gt;

&lt;p&gt;This guide compares six headless platforms specifically as WordPress replacements. We make Cosmic, so weigh our take on it accordingly. Every price below links to the vendor page it was read from, and platforms are listed alphabetically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CMS&lt;/th&gt;
&lt;th&gt;Best for teams coming from WordPress who...&lt;/th&gt;
&lt;th&gt;Editor feel&lt;/th&gt;
&lt;th&gt;Migration help&lt;/th&gt;
&lt;th&gt;Paid plans from&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cosmic&lt;/td&gt;
&lt;td&gt;...want a hosted CMS and someone to move the content&lt;/td&gt;
&lt;td&gt;Structured forms, AI agents&lt;/td&gt;
&lt;td&gt;Free hand migration on paid plans&lt;/td&gt;
&lt;td&gt;$99/month (Builder) (&lt;a href="https://www.cosmicjs.com/pricing" rel="noopener noreferrer"&gt;source&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payload&lt;/td&gt;
&lt;td&gt;...want to self-host, like WordPress&lt;/td&gt;
&lt;td&gt;Structured forms in your own app&lt;/td&gt;
&lt;td&gt;Do it yourself&lt;/td&gt;
&lt;td&gt;Free to self-host; Cloud prices no longer published (&lt;a href="https://payloadcms.com/cloud-pricing" rel="noopener noreferrer"&gt;source&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prismic&lt;/td&gt;
&lt;td&gt;...build marketing pages from sections&lt;/td&gt;
&lt;td&gt;Page Builder with slices&lt;/td&gt;
&lt;td&gt;Import API&lt;/td&gt;
&lt;td&gt;$10/month (Starter, annual) (&lt;a href="https://prismic.io/pricing" rel="noopener noreferrer"&gt;source&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sanity&lt;/td&gt;
&lt;td&gt;...have developers to build a custom editor&lt;/td&gt;
&lt;td&gt;Customizable Studio&lt;/td&gt;
&lt;td&gt;Migration tooling for developers&lt;/td&gt;
&lt;td&gt;$15/seat/month (Growth) (&lt;a href="https://www.sanity.io/pricing" rel="noopener noreferrer"&gt;source&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storyblok&lt;/td&gt;
&lt;td&gt;...loved a visual page builder&lt;/td&gt;
&lt;td&gt;Visual, drag-and-drop&lt;/td&gt;
&lt;td&gt;Migration guides and partners&lt;/td&gt;
&lt;td&gt;$99/month (Growth) (&lt;a href="https://www.storyblok.com/pricing" rel="noopener noreferrer"&gt;source&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strapi&lt;/td&gt;
&lt;td&gt;...want open source and self-hosting&lt;/td&gt;
&lt;td&gt;Structured forms you host&lt;/td&gt;
&lt;td&gt;Community plugins&lt;/td&gt;
&lt;td&gt;Free to self-host; Cloud from $35/month (&lt;a href="https://strapi.io/pricing-cloud" rel="noopener noreferrer"&gt;source&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What a WordPress migration involves
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Content export.&lt;/strong&gt; The &lt;a href="https://developer.wordpress.org/rest-api/reference/posts/" rel="noopener noreferrer"&gt;WordPress REST API&lt;/a&gt; (&lt;code&gt;/wp-json/wp/v2/posts&lt;/code&gt;, &lt;code&gt;/pages&lt;/code&gt;, &lt;code&gt;/media&lt;/code&gt;) gives you clean JSON, including posts behind custom post types if they are registered with &lt;a href="https://developer.wordpress.org/reference/functions/register_post_type/" rel="noopener noreferrer"&gt;&lt;code&gt;show_in_rest&lt;/code&gt;&lt;/a&gt;. It is usually easier than the XML export.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom fields.&lt;/strong&gt; Advanced Custom Fields (ACF) and similar plugins hold much of the real structure, on top of WordPress's own &lt;a href="https://developer.wordpress.org/reference/functions/register_post_type/" rel="noopener noreferrer"&gt;&lt;code&gt;custom-fields&lt;/code&gt; post type support&lt;/a&gt;. Map each field group to a content type in the new CMS before importing anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shortcodes and page builder markup.&lt;/strong&gt; Content built with Elementor, Divi, or WPBakery stores layout as &lt;a href="https://developer.wordpress.org/apis/shortcode/" rel="noopener noreferrer"&gt;shortcodes&lt;/a&gt; or proprietary markup. That does not convert cleanly. Plan to rebuild those pages as components, and migrate only the posts that are plain content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Media.&lt;/strong&gt; Download every &lt;a href="https://developer.wordpress.org/rest-api/reference/media/" rel="noopener noreferrer"&gt;media attachment&lt;/a&gt;, upload it to the new CMS, and rewrite image URLs inside post bodies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;URLs and SEO.&lt;/strong&gt; Keep slugs identical where you can, and set 301 redirects for anything that changes, including &lt;a href="https://wordpress.org/documentation/article/customize-permalinks/" rel="noopener noreferrer"&gt;date-based permalinks&lt;/a&gt;, category archives, and &lt;code&gt;/feed&lt;/code&gt;. Carry over SEO titles and descriptions from Yoast or Rank Math.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Editors.&lt;/strong&gt; WordPress users are used to a &lt;a href="https://wordpress.org/documentation/article/dashboard-screen/" rel="noopener noreferrer"&gt;single admin dashboard&lt;/a&gt;. Choose a CMS whose editor your team will actually accept, and give them a preview of drafts on the real site.&lt;/p&gt;

&lt;h2&gt;
  
  
  The options
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cosmic
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;As a WordPress replacement:&lt;/strong&gt; hosted, so there is no server, plugin, or security patching to manage. Content types are built in the dashboard, which maps naturally from ACF field groups. AI agents can draft, edit, and translate content, and images are served through imgix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration:&lt;/strong&gt; on any paid plan, our team migrates your WordPress content for you at no extra cost. We model your content types, import posts, pages, and media, and check the result with you. You can also &lt;a href="https://www.cosmicjs.com/migrate?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=best-headless-cms-to-replace-wordpress&amp;amp;utm_content=body-migrate" rel="noopener noreferrer"&gt;analyze your site&lt;/a&gt; to see what a migration would involve, or follow our &lt;a href="https://www.cosmicjs.com/blog/migrate-from-wordpress-to-cosmic?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=best-headless-cms-to-replace-wordpress&amp;amp;utm_content=body-guide" rel="noopener noreferrer"&gt;step-by-step WordPress migration guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing:&lt;/strong&gt; Free is $0 with 1 Bucket, 2 team members, and 1,000 Objects. Builder is $99/month, Team $299/month, and Business $499/month. Each plan includes a set number of team members, and additional members are $29 each per month (&lt;a href="https://www.cosmicjs.com/pricing" rel="noopener noreferrer"&gt;Cosmic pricing&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch for:&lt;/strong&gt; there is no drag-and-drop page builder, so pages built in Elementor or Divi become developer-built components. You also need a frontend; Cosmic does not render your site.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Payload
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;As a WordPress replacement:&lt;/strong&gt; the most WordPress-like in spirit: open source, self-hosted, and fully under your control, with an admin panel inside your Next.js app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing:&lt;/strong&gt; free to self-host. Payload's Cloud pricing page no longer publishes plan prices, and new Cloud project deployments are paused following Payload joining Figma (&lt;a href="https://payloadcms.com/cloud-pricing" rel="noopener noreferrer"&gt;Payload Cloud pricing&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch for:&lt;/strong&gt; you keep the operational work you were trying to leave behind (hosting, database, backups, upgrades).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Prismic
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;As a WordPress replacement:&lt;/strong&gt; slices give editors a page-building experience closer to WordPress blocks, with developer control over each section.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing:&lt;/strong&gt; there is a free tier. Paid plans are priced per repository and billed annually: Starter $10/month, Small $25/month, Medium $150/month, and Platinum $675/month (&lt;a href="https://prismic.io/pricing" rel="noopener noreferrer"&gt;Prismic pricing&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch for:&lt;/strong&gt; blog-heavy sites with complex taxonomies fit less naturally than page-led marketing sites.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Sanity
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;As a WordPress replacement:&lt;/strong&gt; the most customizable editing experience, with real-time collaboration and strong developer tooling for scripted imports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing:&lt;/strong&gt; there is a free tier. Growth is $15 per seat per month, and Enterprise is custom priced (&lt;a href="https://www.sanity.io/pricing" rel="noopener noreferrer"&gt;Sanity pricing&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch for:&lt;/strong&gt; the Studio is configured in code, so editors depend on developers for changes, and rich text converts to Portable Text.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Storyblok
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;As a WordPress replacement:&lt;/strong&gt; the closest to a WordPress page builder: editors drag components onto a page and see the result in place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing:&lt;/strong&gt; Starter is free. Growth is $99/month and Growth Plus is $349/month, with additional seats at $15 each per month on those plans (&lt;a href="https://www.storyblok.com/pricing" rel="noopener noreferrer"&gt;Storyblok pricing&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch for:&lt;/strong&gt; seat limits for larger editorial teams, and a component-first model that suits pages more than a large post archive.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Strapi
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;As a WordPress replacement:&lt;/strong&gt; open source and self-hosted like WordPress, with content types built in an admin UI and a plugin marketplace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing:&lt;/strong&gt; free to self-host. Strapi Cloud starts at $35 per project per month (Starter), with Pro at $90 and Business at $450 (&lt;a href="https://strapi.io/pricing-cloud" rel="noopener noreferrer"&gt;Strapi Cloud pricing&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch for:&lt;/strong&gt; self-hosting brings back server and security maintenance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your editors live in a page builder:&lt;/strong&gt; Storyblok, or Prismic for section-based pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want to keep self-hosting, just headless:&lt;/strong&gt; Strapi or Payload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You have developers to build a tailored editor:&lt;/strong&gt; Sanity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want to stop running servers and have the migration done for you:&lt;/strong&gt; Cosmic.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you want to see what your own WordPress site looks like as structured content, you can model it on the Cosmic free plan today, no credit card required.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://app.cosmicjs.com/signup?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=best-headless-cms-to-replace-wordpress&amp;amp;utm_content=bottom-cta" rel="noopener noreferrer"&gt;Start free&lt;/a&gt; or &lt;a href="https://calendly.com/tonyspiro/cosmic-intro" rel="noopener noreferrer"&gt;book 15 minutes with our CEO&lt;/a&gt; to walk through your migration.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.cosmicjs.com/blog/best-headless-cms-to-replace-wordpress?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=best-headless-cms-to-replace-wordpress" rel="noopener noreferrer"&gt;Cosmic blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>cms</category>
      <category>webdev</category>
      <category>headless</category>
    </item>
    <item>
      <title>Connect a Cosmic Agent to Google Sheets</title>
      <dc:creator>Tony Spiro</dc:creator>
      <pubDate>Fri, 25 Sep 2026 15:28:20 +0000</pubDate>
      <link>https://dev.to/tonyspiro/connect-a-cosmic-agent-to-google-sheets-58lf</link>
      <guid>https://dev.to/tonyspiro/connect-a-cosmic-agent-to-google-sheets-58lf</guid>
      <description>&lt;p&gt;Letting an agent write to a Google Sheet is a small capability with a surprisingly sharp setup. The auth is a service account, the API is public and well documented, and the create payload is three fields. The working version also calls a different Google API than the task description implies.&lt;/p&gt;

&lt;p&gt;This is the setup end to end, written so you can follow it with your own Google account and your own Cosmic agent. If a step fails on you, the Troubleshooting section at the end covers what this path actually produces.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you will have at the end
&lt;/h2&gt;

&lt;p&gt;An agent that can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a new spreadsheet in a location your team owns&lt;/li&gt;
&lt;li&gt;Write multiple tabs into it in one pass, some tabular, some prose&lt;/li&gt;
&lt;li&gt;Share it with a human reviewer&lt;/li&gt;
&lt;li&gt;Read that spreadsheet back later, including whatever the human changed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last item is what makes this worth wiring up. A one-way export is a file. A sheet the agent can re-read is a two-way surface between a run and a person.&lt;/p&gt;

&lt;p&gt;You need a Google account that can create a service account and a Shared Drive, which in practice means Google Workspace rather than a personal Gmail account. You also need a Cosmic agent with the API request capability enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Set up the Google side
&lt;/h2&gt;

&lt;p&gt;Four things, in this order, in the &lt;a href="https://console.cloud.google.com/" rel="noopener noreferrer"&gt;Google Cloud console&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create a project, or pick an existing one.&lt;/strong&gt; The API enablement and the service account below both happen inside this project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Enable both APIs.&lt;/strong&gt; Google Sheets API and Google Drive API. Both, because the call that actually creates the file is a Drive call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Create a service account and download its JSON key.&lt;/strong&gt; IAM and Admin, Service Accounts, Create Service Account, then Keys, Add Key, JSON. The file you get contains a &lt;code&gt;client_email&lt;/code&gt;, a &lt;code&gt;private_key&lt;/code&gt;, and the project identifiers. Treat it like a password. It is one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Create a Shared Drive, then add the service account to it as a member.&lt;/strong&gt; Google Drive, Shared drives, Create shared drive. Open Manage members, paste the service account's &lt;code&gt;client_email&lt;/code&gt;, and grant it Content manager or Manager. Copy the Shared Drive ID out of the URL when you open the drive: it is the segment after &lt;code&gt;/drive/folders/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Set up the Cosmic side
&lt;/h2&gt;

&lt;p&gt;Three pieces, all of which live in your own Cosmic account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Store the key as a secret.&lt;/strong&gt; Ask your agent to save the service account JSON with &lt;code&gt;manage_secrets&lt;/code&gt;. It gets stored encrypted and referenced by id, so it never appears in a prompt, a log, or a saved request body. This is the part people get wrong by pasting a private key into an agent instruction and hoping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Save the Google calls as endpoints.&lt;/strong&gt; Your agent can register saved API endpoints with default headers, so the Drive create, the Sheets batch update, the values write, and the read-back each become a named endpoint instead of a URL it has to reconstruct from memory every run. Reference the token in the default &lt;code&gt;Authorization&lt;/code&gt; header as a secret placeholder rather than a literal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Call them with &lt;code&gt;api_request&lt;/code&gt;.&lt;/strong&gt; From there the agent makes plain HTTPS calls to &lt;code&gt;googleapis.com&lt;/code&gt;. There is no Cosmic-side Google integration doing anything clever underneath. Every call in the rest of this post is a call you could paste into curl.&lt;/p&gt;

&lt;p&gt;One honest rough edge before you start. A service account authenticates by signing a JWT with RS256 and exchanging it at &lt;code&gt;https://oauth2.googleapis.com/token&lt;/code&gt; for an access token that lasts an hour. The exchange is a plain HTTP POST your agent can make. The RS256 signing is not something an agent does with an HTTP tool alone. Two practical ways through it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For following this walkthrough:&lt;/strong&gt; mint a token once from your own machine and store that token as a secret. &lt;code&gt;gcloud auth print-access-token --impersonate-service-account=YOUR_SERVICE_ACCOUNT_EMAIL&lt;/code&gt; does it in one line, as does a five-line Node script using &lt;code&gt;google-auth-library&lt;/code&gt;. It expires in an hour, which is plenty to work through these steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For anything running on a schedule:&lt;/strong&gt; put the signing in a small endpoint you own, have it return a fresh access token, and register that as one more saved endpoint the agent calls before the Google calls. Store its shared secret with &lt;code&gt;manage_secrets&lt;/code&gt; the same way.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Request both scopes on every token, not just the one matching the call you think you are making:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://www.googleapis.com/auth/spreadsheets
https://www.googleapis.com/auth/drive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Creation happens through Drive and writing happens through Sheets, so a token carrying both keeps a single token path across every call in this post. The broader scope is acceptable here because the identity holding it is a service account whose only access is the one Shared Drive you added it to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Create the file through the Drive API
&lt;/h2&gt;

&lt;p&gt;Create the spreadsheet with a Drive call rather than &lt;code&gt;sheets.spreadsheets.create&lt;/code&gt;, because a service account has no Drive storage quota of its own and therefore has to create the file inside a Shared Drive it has been added to as a member.&lt;/p&gt;

&lt;p&gt;This is the call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://www.googleapis.com/drive/v3/files?supportsAllDrives=true&amp;amp;fields=id,webViewLink
Authorization: Bearer &amp;lt;token&amp;gt;
Content-Type: application/json

{
  "name": "Weekly content report",
  "mimeType": "application/vnd.google-apps.spreadsheet",
  "parents": ["YOUR_SHARED_DRIVE_ID"]
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response gives you an &lt;code&gt;id&lt;/code&gt;, which is the spreadsheet id every later Sheets call uses, and a &lt;code&gt;webViewLink&lt;/code&gt;, which is the URL you hand to a human.&lt;/p&gt;

&lt;p&gt;Three parts of that request carry weight. &lt;code&gt;mimeType&lt;/code&gt; is what makes Drive mint a native Google Sheets file the Sheets API can address by id, rather than an opaque blob. &lt;code&gt;parents&lt;/code&gt; assigns the file to the Shared Drive, so the Drive owns it and the service account is simply the thing that created it. &lt;code&gt;supportsAllDrives=true&lt;/code&gt; is required on every Drive call that touches a file in a Shared Drive, including this one and the permissions call below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Write multiple tabs, then read the edits back
&lt;/h2&gt;

&lt;p&gt;The first tab exists as soon as the file does. Every additional tab is two calls.&lt;/p&gt;

&lt;p&gt;Add the sheet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://sheets.googleapis.com/v4/spreadsheets/&amp;lt;id&amp;gt;:batchUpdate
Authorization: Bearer &amp;lt;token&amp;gt;
Content-Type: application/json

{
  "requests": [
    { "addSheet": { "properties": { "title": "Code snippets" } } }
  ]
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then write values into it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;PUT https://sheets.googleapis.com/v4/spreadsheets/&amp;lt;id&amp;gt;/values/Code%20snippets!A1?valueInputOption=RAW
Authorization: Bearer &amp;lt;token&amp;gt;
Content-Type: application/json

{
  "values": [["Slug", "Status"], ["pricing", "live"]]
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;values&lt;/code&gt; is an array of rows, each row an array of cells. For narrative content rather than a table, split the prose on blank lines and make each paragraph a single-cell row, which puts one paragraph per row in column A.&lt;/p&gt;

&lt;p&gt;Two practical notes. Sheet names with spaces have to be URL-encoded in the range, which is the &lt;code&gt;Code%20snippets!A1&lt;/code&gt; above. And when you are writing a dozen tabs, collect per-tab failures rather than aborting the whole job on the first bad one. A partial write that reports what it could not do beats losing eleven good tabs because the twelfth had an invalid name.&lt;/p&gt;

&lt;p&gt;Share it with a human using the Drive permissions endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://www.googleapis.com/drive/v3/files/&amp;lt;id&amp;gt;/permissions?supportsAllDrives=true&amp;amp;sendNotificationEmail=false
Authorization: Bearer &amp;lt;token&amp;gt;
Content-Type: application/json

{ "role": "writer", "type": "user", "emailAddress": "reviewer@yourcompany.com" }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note &lt;code&gt;supportsAllDrives=true&lt;/code&gt; again. It is required on this call for the same reason it was required on the create.&lt;/p&gt;

&lt;p&gt;Then read the whole thing back, including whatever the human changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://sheets.googleapis.com/v4/spreadsheets/&amp;lt;id&amp;gt;?includeGridData=true&amp;amp;fields=properties.title,sheets(properties.title,data.rowData.values.formattedValue)
Authorization: Bearer &amp;lt;token&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The narrow &lt;code&gt;fields&lt;/code&gt; mask is the detail worth copying. Without it, &lt;code&gt;includeGridData=true&lt;/code&gt; returns the entire grid including every formatting property on every cell, and the payload gets very large very fast on a sheet a human has been editing for a week. With the mask you get titles and &lt;code&gt;formattedValue&lt;/code&gt; strings and nothing else. Normalize missing cells to an empty string on your side so whatever consumes this never has to null-check a cell.&lt;/p&gt;

&lt;p&gt;That read-back closes the loop. Your agent wrote a draft, a person edited it in a tool they already had open with no new login and no new app, and the next run can see exactly what they left behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things this makes possible
&lt;/h2&gt;

&lt;p&gt;Creating a file is a small capability with a large operational consequence attached. The destination has to be a Shared Drive the team owns, because the service account cannot own anything itself. That same constraint shows up in any content operation spanning many properties: one shared credential writing into one owned destination, rather than a separate identity and a separate orphaned folder per property.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. A draft handoff a human can actually edit
&lt;/h3&gt;

&lt;p&gt;An agent writes a draft into a sheet and shares it with a reviewer. The reviewer edits in place. A later run reads the spreadsheet back and picks up exactly what the human left behind. The read-back is what turns the sheet into a two-way surface instead of an export.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Structured exports that are not one flat table
&lt;/h3&gt;

&lt;p&gt;Most real reports have more than one shape in them. A content audit has a table of pages with statuses, and it also has a pile of prose that does not fit in a grid. Multi-tab writing handles both in one pass: rows for the tabular tab, paragraph-per-row for the narrative tab. Collecting per-tab failures means a twelve-tab export that hits one bad tab name still delivers eleven tabs and tells you which one it dropped.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. One credential across many properties
&lt;/h3&gt;

&lt;p&gt;This is the use worth the most attention, and it is where the Shared Drive constraint stops being an annoyance and starts being the point.&lt;/p&gt;

&lt;p&gt;A team running five to twenty distinct web properties has the same reporting job repeated per property: pull what changed, write it somewhere a human will actually read, keep the history. The naive version gives every property its own credential, its own destination folder, and its own half-remembered setup. Six months later nobody knows which service account owns which folder, and the person who configured property nine has left the company.&lt;/p&gt;

&lt;p&gt;Creating through Drive with an explicit &lt;code&gt;parents&lt;/code&gt; forces the better shape by default. The file is owned by a Shared Drive from the moment it exists. Access is governed by the Drive permissions the team already maintains, not by whoever happens to hold the key. Removing someone from the Drive removes them from every sheet the agent has ever written, across every property, without touching the agent or rotating anything. Adding a property means passing a different parent folder id on the request and provisioning no new identity at all.&lt;/p&gt;

&lt;p&gt;The read-back compounds this. Once each sheet is a real addressable destination with a stable id, a run next week can read what a human changed this week and act on it, which holds across properties and across time rather than only within a single job. If your team is already running a dozen properties, the same consolidation argument applies one level up to the content layer itself: &lt;a href="https://www.cosmicjs.com/workspaces?utm_source=dev.to&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=connect-agent-to-google-sheets&amp;amp;utm_content=body-workspaces"&gt;Workspaces&lt;/a&gt; puts every brand on one bill and one team.&lt;/p&gt;

&lt;p&gt;Where this stops is worth naming plainly. The capability is that an agent can create, write to, share, and read a spreadsheet in a location the team owns. What gets built on top of it, whether per-property reporting, a review queue, or a staging area for bulk import, is a workflow decision you make. The setup guarantees the destination is owned by the team and reachable by the next run, and deliberately assumes nothing beyond that.&lt;/p&gt;

&lt;p&gt;If you are wiring agents into systems that write, the same question applies to your content layer: what exactly is the credential allowed to do, and can you verify it rather than assume it. We wrote up the Cosmic answer in &lt;a href="https://www.cosmicjs.com/blog/ai-agent-write-access-cms-controls?utm_source=dev.to&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=connect-agent-to-google-sheets&amp;amp;utm_content=body-write-access-controls"&gt;AI Agent Write Access: The 4 Real Controls You Get With Your CMS&lt;/a&gt;, including the two controls we do not ship yet. The broader picture of agents reading and writing structured content is on the &lt;a href="https://www.cosmicjs.com/ai?utm_source=dev.to&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=connect-agent-to-google-sheets&amp;amp;utm_content=body-ai-page"&gt;Cosmic for AI teams&lt;/a&gt; page, and you can &lt;a href="https://app.cosmicjs.com/signup?utm_source=dev.to&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=connect-agent-to-google-sheets&amp;amp;utm_content=body-signup"&gt;start on the Free plan&lt;/a&gt;, no credit card required, and give an agent its first saved endpoint in about ten minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;403 PERMISSION_DENIED&lt;/code&gt; on &lt;code&gt;POST /v4/spreadsheets&lt;/code&gt;.&lt;/strong&gt; The service account has no Drive storage of its own, so it cannot own the file that call is asking Google to create. Create the spreadsheet through the Drive API instead, into a Shared Drive the service account has been added to as a member, and pass &lt;code&gt;supportsAllDrives=true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A reviewer cannot open the sheet.&lt;/strong&gt; The file inherits the permissions of the Shared Drive it was created in, so anyone who is not a member of that Drive has no access until you share the file explicitly. Grant it with the Drive permissions call in Step 4.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A write lands somewhere other than the tab you expected.&lt;/strong&gt; The tab name in the range has to match a sheet that already exists, and a range with no tab name goes to the first sheet in the file. Add the tab with &lt;code&gt;batchUpdate&lt;/code&gt; before writing to it, and URL-encode any spaces in the name.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>tutorial</category>
      <category>googlesheets</category>
    </item>
    <item>
      <title>Give your agent an inbox, then sign it up for Cosmic</title>
      <dc:creator>Tony Spiro</dc:creator>
      <pubDate>Wed, 23 Sep 2026 14:55:04 +0000</pubDate>
      <link>https://dev.to/tonyspiro/give-your-agent-an-inbox-then-sign-it-up-for-cosmic-32n0</link>
      <guid>https://dev.to/tonyspiro/give-your-agent-an-inbox-then-sign-it-up-for-cosmic-32n0</guid>
      <description>&lt;p&gt;Hand an agent a task like "build me a recipe blog" and it can scaffold the app, write the content, and ship it. Then it hits a wall. The CMS wants an account, the account wants an email address, and the verification code lands in a human's mail client. The run stops and waits for someone to paste six digits back.&lt;/p&gt;

&lt;p&gt;This tutorial removes that stop. The agent provisions its own email inbox from &lt;a href="https://www.agentmail.to/" rel="noopener noreferrer"&gt;AgentMail&lt;/a&gt;, passes that address to &lt;a href="https://www.cosmicjs.com/docs/api/agents" rel="noopener noreferrer"&gt;Cosmic agent signup&lt;/a&gt; as &lt;code&gt;human_email&lt;/code&gt;, reads the claim code out of its own inbox, and submits it to verify. Working curl for every step, and no human in the loop.&lt;/p&gt;

&lt;p&gt;Why it is worth wiring up: until that code is submitted, the new project sits in a restricted state with a deletion clock running on it. An agent that can read its own mail clears the gate itself and finishes the run holding a verified Cosmic project, a bucket, and the read and write keys to start writing content to it.&lt;/p&gt;

&lt;p&gt;This is for developers building an agent (Cursor, Claude, a backend service) that needs a real content backend and can be handed an email address. You need a domain you can receive mail on, and nothing else.&lt;/p&gt;

&lt;p&gt;The flow below was run against the production APIs on September 22, 2026. Signup returned an unclaimed project, the claim code arrived in the AgentMail message preview, and verify returned &lt;code&gt;auth_type: verified&lt;/code&gt; with the restricted limits lifted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get at the end
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An AgentMail inbox at &lt;code&gt;my-agent@agentmail.to&lt;/code&gt;, plus an API key the agent uses to read it&lt;/li&gt;
&lt;li&gt;A Cosmic project and bucket in &lt;code&gt;auth_type: verified&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Bucket &lt;code&gt;read_key&lt;/code&gt; and &lt;code&gt;write_key&lt;/code&gt;, ready to pass to the SDK&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;agent_key&lt;/code&gt; (&lt;code&gt;agk_...&lt;/code&gt;) for later status checks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Between signup and verify the bucket sits in an unclaimed state, which you pass straight through rather than stop in. While unclaimed it allows 50 objects and 5 MB of media, AI generation is off, and the project is hard-deleted after 14 days if nobody claims it. Verifying lifts all of that.&lt;/p&gt;

&lt;p&gt;Reference docs: &lt;a href="https://www.cosmicjs.com/docs/api/agents" rel="noopener noreferrer"&gt;Cosmic agent signup&lt;/a&gt; and the &lt;a href="https://www.agentmail.to/docs/quickstart" rel="noopener noreferrer"&gt;AgentMail quickstart&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two different email addresses
&lt;/h2&gt;

&lt;p&gt;This is the part that trips people up, so settle it before touching any code. Two addresses are in play and they do different jobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The AgentMail owner address.&lt;/strong&gt; A real mailbox on a normal domain, something like &lt;code&gt;you@yourdomain.com&lt;/code&gt;. You pass it as &lt;code&gt;human_email&lt;/code&gt; on &lt;code&gt;POST /v0/agent/sign-up&lt;/code&gt;, once, the first time this human creates an AgentMail account. AgentMail sends its own 6-digit OTP there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The agent inbox.&lt;/strong&gt; Created by that signup, in the form &lt;code&gt;username@agentmail.to&lt;/code&gt;. This is the address you hand to Cosmic as &lt;code&gt;human_email&lt;/code&gt;. Cosmic's claim email lands here, and the agent reads it with the AgentMail API key.&lt;/p&gt;

&lt;p&gt;Using one address for both jobs fails. AgentMail rejects &lt;code&gt;@agentmail.to&lt;/code&gt; as an owner address, and pointing Cosmic at your personal mailbox puts the claim code somewhere the agent cannot reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the AgentMail inbox
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.agentmail.to/v0/agent/sign-up &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "human_email": "you@yourdomain.com",
    "username": "my-agent",
    "source": "cursor"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response carries &lt;code&gt;api_key&lt;/code&gt;, &lt;code&gt;inbox_id&lt;/code&gt;, and &lt;code&gt;organization_id&lt;/code&gt;. With &lt;code&gt;username: "my-agent"&lt;/code&gt; the inbox is &lt;code&gt;my-agent@agentmail.to&lt;/code&gt;, and &lt;code&gt;inbox_id&lt;/code&gt; is that same email address. Store the API key somewhere durable, because it is not shown again.&lt;/p&gt;

&lt;p&gt;AgentMail also emails a 6-digit OTP to &lt;code&gt;you@yourdomain.com&lt;/code&gt;. Submit it to unlock full permissions on the key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.agentmail.to/v0/agent/verify &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AGENTMAIL_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{ "otp_code": "123456" }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The field here is &lt;code&gt;otp_code&lt;/code&gt;. Keep that in mind, because Cosmic's verify endpoint names the same concept differently in a later step.&lt;/p&gt;

&lt;p&gt;One observation from the September 22 run: the key returned by signup was already able to list inbox messages before this OTP was submitted. Do not build on that. AgentMail documents the key as limited until verification, so treat verifying as part of setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Errors you will actually hit
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@agentmail.to&lt;/code&gt; as the owner address&lt;/strong&gt; returns &lt;code&gt;403&lt;/code&gt; with &lt;code&gt;code: "forbidden"&lt;/code&gt; and the message &lt;code&gt;Domain is forbidden&lt;/code&gt;. The owner address has to be an ordinary mailbox.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disposable inbox domains&lt;/strong&gt; return the same &lt;code&gt;403&lt;/code&gt; and &lt;code&gt;forbidden&lt;/code&gt; code. Use a real domain you control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An address that already has an AgentMail account&lt;/strong&gt; returns &lt;code&gt;403&lt;/code&gt; with &lt;code&gt;code: "already_exists"&lt;/code&gt; and the message &lt;code&gt;User already exists&lt;/code&gt;, and no new API key comes back. Plus-aliases count as the same user, so &lt;code&gt;you+agent@yourdomain.com&lt;/code&gt; collides with &lt;code&gt;you@yourdomain.com&lt;/code&gt;. When the human already has an account, skip signup entirely: use their existing API key and create the mailbox with &lt;code&gt;POST /v0/inboxes&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Branch on the &lt;code&gt;code&lt;/code&gt; field rather than the message text. AgentMail documents those codes as stable and the prose as subject to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sign up for Cosmic with the agent inbox
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://dapi.cosmicjs.com/v3/agents/sign-up &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "human_email": "my-agent@agentmail.to",
    "project_name": "Recipe Blog",
    "agent_id": "cursor"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here &lt;code&gt;human_email&lt;/code&gt; has to be the AgentMail inbox. The owner's personal address belongs only to the AgentMail signup in the previous step.&lt;/p&gt;

&lt;p&gt;The response contains &lt;code&gt;agent_key&lt;/code&gt; (prefixed &lt;code&gt;agk_&lt;/code&gt;), &lt;code&gt;access_token&lt;/code&gt;, &lt;code&gt;auth_type: "unclaimed"&lt;/code&gt;, the &lt;code&gt;project&lt;/code&gt;, &lt;code&gt;bucket.slug&lt;/code&gt;, &lt;code&gt;bucket.read_key&lt;/code&gt;, &lt;code&gt;bucket.write_key&lt;/code&gt;, a &lt;code&gt;claim_url&lt;/code&gt;, and the &lt;code&gt;limits&lt;/code&gt; object. Persist the &lt;code&gt;agent_key&lt;/code&gt; and the bucket keys. If your sample code writes state to disk, keep those values out of anything you log.&lt;/p&gt;

&lt;p&gt;Cosmic then emails the agent inbox. The subject reads &lt;code&gt;An AI agent created a Cosmic project for you: "Recipe Blog"&lt;/code&gt;, and the plain-text body contains the line &lt;code&gt;Your one-time claim code is 123456 (expires in 15 minutes).&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Two behaviors worth coding for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Calling signup again with the same &lt;code&gt;human_email&lt;/code&gt; and &lt;code&gt;agent_id&lt;/code&gt; is idempotent. You get the same project back and a fresh code, which is the correct recovery path for an expired code.&lt;/li&gt;
&lt;li&gt;If that email already belongs to a human Cosmic account, the response is &lt;code&gt;409&lt;/code&gt; with &lt;code&gt;code: "user_already_exists"&lt;/code&gt; and a &lt;code&gt;claim_existing_url&lt;/code&gt;. Stop there and surface the URL. Retrying with a different email is the wrong move and leaves stray projects behind.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Read the claim code from the inbox
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://api.agentmail.to/v0/inboxes/my-agent@agentmail.to/messages?subject=Cosmic&amp;amp;limit=20"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AGENTMAIL_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;inbox_id&lt;/code&gt; path segment is the email address itself. The &lt;code&gt;subject&lt;/code&gt; query parameter does a substring match server side, so the agent can ask for the Cosmic message directly instead of scanning everything that arrives. Drop the filter if you would rather poll the whole list and match client side.&lt;/p&gt;

&lt;p&gt;Messages come back newest first, each with &lt;code&gt;message_id&lt;/code&gt;, &lt;code&gt;subject&lt;/code&gt;, and an optional &lt;code&gt;preview&lt;/code&gt;. In the live run the 6-digit code was sitting in &lt;code&gt;preview&lt;/code&gt;. When the preview is empty or cut short, fetch the single message at &lt;code&gt;GET /v0/inboxes/{inbox_id}/messages/{message_id}&lt;/code&gt; and read its &lt;code&gt;text&lt;/code&gt; body.&lt;/p&gt;

&lt;p&gt;Match on the labeled code rather than the first six digits on the page, because a timestamp or an ID can beat the real code to the regex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/claim code is&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;(\d{6})&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code expires after 15 minutes. If your poll loop runs past that, call Cosmic signup again with the same email and &lt;code&gt;agent_id&lt;/code&gt; and read the fresh code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the Cosmic project
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://dapi.cosmicjs.com/v3/agents/verify &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$COSMIC_AGENT_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{ "code": "123456" }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cosmic names this field &lt;code&gt;code&lt;/code&gt;. AgentMail named the equivalent field &lt;code&gt;otp_code&lt;/code&gt; back in step one. Sending the wrong key is an easy mistake when both calls sit in the same script.&lt;/p&gt;

&lt;p&gt;A successful call returns &lt;code&gt;Verified. Restricted-mode limits lifted.&lt;/code&gt; along with &lt;code&gt;auth_type: "verified"&lt;/code&gt;. Confirm it independently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://dapi.cosmicjs.com/v3/agents/status &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$COSMIC_AGENT_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;auth_type&lt;/code&gt; reads &lt;code&gt;verified&lt;/code&gt; and &lt;code&gt;limits&lt;/code&gt; comes back &lt;code&gt;null&lt;/code&gt;. The bucket is now on standard free-plan limits, AI generation included.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the bucket
&lt;/h2&gt;

&lt;p&gt;From here it is an ordinary Cosmic bucket, with one step that trips people up. A bucket created by agent signup is empty. It has no Object types in it, and an Object cannot exist without a type to live in. So the agent models the content first, then writes to it.&lt;/p&gt;

&lt;p&gt;Pass the &lt;code&gt;slug&lt;/code&gt;, &lt;code&gt;read_key&lt;/code&gt;, and &lt;code&gt;write_key&lt;/code&gt; from the signup response to &lt;code&gt;createBucketClient&lt;/code&gt;. The write key is what authorizes both calls below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createBucketClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@cosmicjs/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cosmic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createBucketClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;bucketSlug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COSMIC_BUCKET_SLUG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;readKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COSMIC_READ_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;writeKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COSMIC_WRITE_KEY&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the Object type. Only &lt;code&gt;title&lt;/code&gt; is required, and the slug defaults to the title converted to a slug. Declare the fields the agent intends to write in &lt;code&gt;metafields&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cosmic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objectTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;singular&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Post&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;📝&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;metafields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;markdown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the object write succeeds, because &lt;code&gt;type: 'posts'&lt;/code&gt; resolves to the type that exists and the &lt;code&gt;content&lt;/code&gt; key in &lt;code&gt;metadata&lt;/code&gt; matches a metafield on it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cosmic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello from an agent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Written by an agent that signed itself up.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to handle if this runs unattended. Object type slugs are unique per bucket, so an agent that might run more than once should call &lt;code&gt;cosmic.objectTypes.find()&lt;/code&gt; first and create the type only when it is missing. And any key you set in &lt;code&gt;metadata&lt;/code&gt; has to exist as a metafield on the type, so adding a field later means a &lt;code&gt;cosmic.objectTypes.updateOne()&lt;/code&gt; call before the write rather than a new key in the object payload. The &lt;a href="https://www.cosmicjs.com/docs/api/object-types" rel="noopener noreferrer"&gt;Object types reference&lt;/a&gt; covers the full set of parameters, and the &lt;a href="https://www.cosmicjs.com/docs/api/metafields" rel="noopener noreferrer"&gt;Metafields reference&lt;/a&gt; lists every field type available.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.cosmicjs.com/docs/api/agents" rel="noopener noreferrer"&gt;agent signup docs&lt;/a&gt; cover the rest of the surface: refreshing &lt;code&gt;access_token&lt;/code&gt;, handing the &lt;code&gt;claim_url&lt;/code&gt; to a human when one is available, and the &lt;code&gt;402&lt;/code&gt; with &lt;code&gt;agent_unclaimed_limit&lt;/code&gt; that you hit if verification gets skipped and the agent keeps writing.&lt;/p&gt;

&lt;p&gt;The whole point of this setup is that the claim code never has to pass through a person. An agent that can receive email can provision its own content backend, verify it, and start writing, in one uninterrupted run.&lt;/p&gt;

&lt;p&gt;Ready to try it? &lt;a href="https://www.cosmicjs.com/signup?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=agentmail-agent-signup&amp;amp;utm_content=closing-cta" rel="noopener noreferrer"&gt;Start free&lt;/a&gt; or &lt;a href="https://www.cosmicjs.com/docs/api/agents" rel="noopener noreferrer"&gt;read the agent API docs&lt;/a&gt;. If you are wiring agents into a larger content operation, &lt;a href="https://calendly.com/tonyspiro/cosmic-intro" rel="noopener noreferrer"&gt;grab time with our CEO&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.cosmicjs.com/blog/agentmail-cosmic-agent-signup" rel="noopener noreferrer"&gt;Cosmic blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Moving a 1,700-page site to the Next.js 16 App Router without a sitemap hole</title>
      <dc:creator>Tony Spiro</dc:creator>
      <pubDate>Mon, 21 Sep 2026 17:13:22 +0000</pubDate>
      <link>https://dev.to/tonyspiro/moving-a-1700-page-site-to-the-nextjs-16-app-router-without-a-sitemap-hole-1lj3</link>
      <guid>https://dev.to/tonyspiro/moving-a-1700-page-site-to-the-nextjs-16-app-router-without-a-sitemap-hole-1lj3</guid>
      <description>&lt;p&gt;Most "we upgraded to Next.js 16" posts are version bumps with a changelog attached. This is not that. This is what it actually took to move cosmicjs.com, a 1,696-URL marketing and docs site backed by &lt;a href="https://www.cosmicjs.com/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=nextjs16-app-router-migration" rel="noopener noreferrer"&gt;Cosmic&lt;/a&gt;, from the Pages Router to the App Router, and the five problems that cost real time.&lt;/p&gt;

&lt;p&gt;None of them were in the migration guide. All five are things you will hit if your site is large, CMS-driven, and deployed on Vercel.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the site
&lt;/h2&gt;

&lt;p&gt;Worth setting the scale, because every problem below is a function of it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1,696 URLs in the sitemap&lt;/li&gt;
&lt;li&gt;686 blog posts and 243 daily Rundown issues, all Objects in Cosmic&lt;/li&gt;
&lt;li&gt;Comparison pages, solutions pages, templates, marketplace listings, integrations, and partners, also all CMS-driven&lt;/li&gt;
&lt;li&gt;MDX docs, and a &lt;code&gt;next-seo&lt;/code&gt; setup with &lt;code&gt;&amp;lt;NextSeo&amp;gt;&lt;/code&gt; calls scattered across a few hundred view components&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The migration ran as a coexistence first. The App Router and Pages Router happily share a repo, so we moved routes in batches and kept &lt;code&gt;pages/&lt;/code&gt; around for the stragglers. That part was uneventful. Then we cut over the rest, and the build stopped finishing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: the build hit Vercel's 45-minute cap
&lt;/h2&gt;

&lt;p&gt;The first full App Router production deploy ran for 46 minutes and then died. Not a crash, not a type error. It hit the ceiling.&lt;/p&gt;

&lt;p&gt;The cause was &lt;code&gt;generateStaticParams&lt;/code&gt;. Under the Pages Router, &lt;code&gt;getStaticPaths&lt;/code&gt; with &lt;code&gt;fallback: 'blocking'&lt;/code&gt; had been quietly doing the right thing for years. When we ported the route files, every dynamic segment got a &lt;code&gt;generateStaticParams&lt;/code&gt; that returned the full slug list, so the build tried to prerender roughly 1,700 pages, each one making its own Cosmic request, inside a 45-minute budget.&lt;/p&gt;

&lt;p&gt;The fix is an environment variable and a guard in every path loader:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loadPaths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GetStaticPaths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SKIP_BUILD_STATIC_GENERATION&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blocking&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ...fetch every slug from Cosmic&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;SKIP_BUILD_STATIC_GENERATION=true&lt;/code&gt; set on Vercel, the build prerenders none of the CMS slug pages. Each one is generated on its first request and then cached, with &lt;code&gt;export const revalidate = 60&lt;/code&gt; on the route keeping it fresh. Build time drops from "does not finish" to a handful of minutes. The first visitor to a cold slug triggers the render and every visitor after that gets a cached page. We measured what that trade actually costs, further down.&lt;/p&gt;

&lt;p&gt;This is the right trade for a content site. It is the wrong trade if you have a hard requirement that every page is warm the instant a deploy finishes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: skipping the prerender quietly broke the sitemap
&lt;/h2&gt;

&lt;p&gt;Here is the part that would have been an SEO incident if we had shipped it without checking.&lt;/p&gt;

&lt;p&gt;We generate &lt;code&gt;sitemap.xml&lt;/code&gt; with &lt;code&gt;next-sitemap&lt;/code&gt; in &lt;code&gt;postbuild&lt;/code&gt;. &lt;code&gt;next-sitemap&lt;/code&gt; works from the Next.js route manifest. It lists what the build produced. So the moment we stopped prerendering the CMS pages, &lt;code&gt;next-sitemap&lt;/code&gt; stopped listing them. The sitemap would have gone from 1,696 URLs to 433, on a site where the blog is the top of the funnel.&lt;/p&gt;

&lt;p&gt;Nothing would have errored. The deploy would have looked perfect.&lt;/p&gt;

&lt;p&gt;The fix is &lt;code&gt;additionalPaths&lt;/code&gt;. Rather than relying on the route manifest, we query Cosmic directly at &lt;code&gt;postbuild&lt;/code&gt; time and inject every published slug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;comparisons&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;solutions&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;fetchAllObjects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blog-posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;slug,modified_at,metadata.last_updated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;fetchAllObjects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;categories&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;slug,id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;isRundownSlug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`/rundown/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/blog/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;extraPaths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;dates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;last_updated&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;modified_at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;next-sitemap&lt;/code&gt; merges &lt;code&gt;additionalPaths&lt;/code&gt; by &lt;code&gt;loc&lt;/code&gt;, so this is safe whether or not the prerender ran. Turn &lt;code&gt;SKIP_BUILD_STATIC_GENERATION&lt;/code&gt; off for a full build and you get no duplicates.&lt;/p&gt;

&lt;p&gt;Two details that are easy to miss:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Paginated listing routes disappear too.&lt;/strong&gt; &lt;code&gt;/blog/page/2&lt;/code&gt; through &lt;code&gt;/blog/page/76&lt;/code&gt; are prerendered pages, not CMS Objects, so they vanish along with everything else. We recompute them from the post count and the page size rather than hardcoding a number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Turn off &lt;code&gt;autoLastmod&lt;/code&gt;.&lt;/strong&gt; Stamping today's date on all 1,696 URLs on every deploy is a signal crawlers learn to discount. We set &lt;code&gt;autoLastmod: false&lt;/code&gt; and pull a real per-post &lt;code&gt;lastmod&lt;/code&gt; from the Cosmic Object's &lt;code&gt;modified_at&lt;/code&gt;, which means the dates in the sitemap now mean something.&lt;/p&gt;

&lt;p&gt;We also added a loud failure. If &lt;code&gt;SKIP_BUILD_STATIC_GENERATION&lt;/code&gt; is on and the Cosmic fetch returns nothing, the build logs an error instead of silently producing a short sitemap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SKIP_BUILD_STATIC_GENERATION&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;extraPaths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[sitemap] SKIP_BUILD_STATIC_GENERATION is on but no Cosmic CMS paths were fetched.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A dry run injected 1,261 paths. The live sitemap now carries 1,263 CMS-driven URLs out of 1,696 total, which is the number that would have quietly disappeared.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3: Turbopack compiled forever in production
&lt;/h2&gt;

&lt;p&gt;With the prerender skipped, the build still hung. Different place this time: "Creating an optimized production build" with no output and no error, until the cap.&lt;/p&gt;

&lt;p&gt;Turbopack is the default bundler for &lt;code&gt;next build&lt;/code&gt; in Next.js 16 and it is genuinely faster in development. On this repo, in Vercel's production build environment, the compile step never finished. Locally it was fine, which is the worst kind of bug.&lt;/p&gt;

&lt;p&gt;We did not root-cause it. We shipped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yarn generate-search-index &amp;amp;&amp;amp; yarn check-types &amp;amp;&amp;amp; next build --webpack"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Webpack finished in minutes. &lt;code&gt;next dev&lt;/code&gt; still uses Turbopack, so we keep the fast feedback loop and give up nothing except a slightly slower CI build. If your production build hangs at compile with no diagnostic output, try &lt;code&gt;--webpack&lt;/code&gt; before you spend a day bisecting your own code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 4: &lt;code&gt;draftMode()&lt;/code&gt; turned every blog post dynamic, then 500'd
&lt;/h2&gt;

&lt;p&gt;Cosmic's live preview needs the page to fetch draft content instead of published content. Under the Pages Router this came through &lt;code&gt;context.preview&lt;/code&gt;. The obvious App Router port is &lt;code&gt;draftMode()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This is the version that broke things&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pageLoaderArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;draft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;draftMode&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;preview&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isEnabled&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;COSMIC_PREVIEW_COOKIE&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;draftMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;preview&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling &lt;code&gt;draftMode()&lt;/code&gt; or &lt;code&gt;cookies()&lt;/code&gt; inside a route opts that route out of static rendering entirely. Every blog post became dynamic, ISR stopped applying, and several slugs started returning 500s in production because the loader was now running in a request context it did not expect.&lt;/p&gt;

&lt;p&gt;The fix is to stop asking the canonical route whether it is in preview. Preview gets its own route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/preview/[...path]/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dynamic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;force-dynamic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That catch-all resolves &lt;code&gt;blog&lt;/code&gt;, &lt;code&gt;rundown&lt;/code&gt;, &lt;code&gt;partners&lt;/code&gt;, &lt;code&gt;customers&lt;/code&gt;, &lt;code&gt;templates&lt;/code&gt;, and marketplace slugs to the same loaders and views the canonical routes use, but runs them with &lt;code&gt;preview: true&lt;/code&gt; and returns &lt;code&gt;robots: { index: false, follow: false }&lt;/code&gt;. The canonical &lt;code&gt;/blog/[slug]&lt;/code&gt; route went back to a plain synchronous argument builder with no &lt;code&gt;cookies()&lt;/code&gt; call anywhere in its tree, which made it statically renderable again.&lt;/p&gt;

&lt;p&gt;The general rule: keep dynamic APIs out of the routes you want cached. If a feature needs per-request state, give it a separate route rather than making your entire content tree dynamic to serve a handful of editor sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 5: &lt;code&gt;next-seo&lt;/code&gt; went silent and took 400 page titles with it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;next-seo&lt;/code&gt; is built on &lt;code&gt;next/head&lt;/code&gt;, which does nothing in the App Router. Not an error, not a warning. The component renders &lt;code&gt;null&lt;/code&gt; and your &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; quietly becomes the layout default.&lt;/p&gt;

&lt;p&gt;We had &lt;code&gt;&amp;lt;NextSeo&amp;gt;&lt;/code&gt; calls in a few hundred view components. Deleting them all as step one of the migration would have meant a very large diff with no way to verify anything in between. Instead we aliased the package to a no-op shim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/lib/next-seo-app.tsx&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;noop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_props&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;NextSeo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;noop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ArticleJsonLd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;noop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...every other export&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aliased in both &lt;code&gt;next.config.mjs&lt;/code&gt; (&lt;code&gt;turbopack.resolveAlias&lt;/code&gt; and the webpack &lt;code&gt;resolve.alias&lt;/code&gt;) and &lt;code&gt;tsconfig.json&lt;/code&gt; paths. Everything compiled, nothing crashed, and every page shipped the default title.&lt;/p&gt;

&lt;p&gt;Then we rebuilt metadata properly with &lt;code&gt;generateMetadata&lt;/code&gt;. The pattern that scaled across 100-plus route files is a single helper that runs the page's existing data loader, picks the SEO fields out of the result, and falls back safely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;metadataFromLoader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loadPage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;defaultSeoPick&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;flattenParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fillPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;loaderArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;notFound&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;picked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;pageMetadata&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;picked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;titleFromPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;picked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;LAYOUT_DEFAULT_DESCRIPTION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;picked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;picked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`generateMetadata failed for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;pageMetadata&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;titleFromPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LAYOUT_DEFAULT_DESCRIPTION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Route files become three lines, with a per-route &lt;code&gt;pick&lt;/code&gt; function for the shapes that differ:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateMetadata&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;metadataFromLoader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loadPage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/blog/[slug]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;articleSeoPick&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;try/catch&lt;/code&gt; matters more than it looks. A metadata function that throws takes the whole page down. Falling back to a title derived from the path means a bad CMS field costs you a weak title, not a 500.&lt;/p&gt;

&lt;p&gt;JSON-LD needed the same treatment. &lt;code&gt;next-seo&lt;/code&gt;'s &lt;code&gt;ArticleJsonLd&lt;/code&gt; was now a no-op, and a &lt;code&gt;'use client'&lt;/code&gt; component that injects a script tag is not reliably visible to crawlers. We render it from the server component instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;JsonLd&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"ld-article"&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;articleJsonLd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`/blog/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The thing that actually de-risked this
&lt;/h2&gt;

&lt;p&gt;We wrote a script that crawls every URL in the sitemap and records status code, &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;, meta description, &lt;code&gt;og:title&lt;/code&gt;, &lt;code&gt;og:description&lt;/code&gt;, &lt;code&gt;og:image&lt;/code&gt;, canonical, and JSON-LD block count. Then we ran it three times: against production before the merge, against the Vercel preview, and against production after the deploy.&lt;/p&gt;

&lt;p&gt;Diffing those three files is what caught the compare pages falling back to a generic &lt;code&gt;Compare | Cosmic&lt;/code&gt; title instead of &lt;code&gt;Contentful vs Cosmic&lt;/code&gt;. It is what confirmed the four slugs that had been 500ing were fixed. It is what proved the sitemap still had all 1,696 URLs.&lt;/p&gt;

&lt;p&gt;It is about 40 lines of &lt;code&gt;curl&lt;/code&gt; and a parser. No Lighthouse run and no amount of clicking around the homepage would have found any of it, because every one of those problems lived on CMS slugs the homepage never links to.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually bought us
&lt;/h2&gt;

&lt;p&gt;Numbers, because "it feels faster" is not a result. These come from the Vercel API across every production deployment of this project.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Window&lt;/th&gt;
&lt;th&gt;Successful builds&lt;/th&gt;
&lt;th&gt;Median&lt;/th&gt;
&lt;th&gt;Mean&lt;/th&gt;
&lt;th&gt;Range&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pages Router, Jul 20 to Sep 18&lt;/td&gt;
&lt;td&gt;85&lt;/td&gt;
&lt;td&gt;6.4 min&lt;/td&gt;
&lt;td&gt;6.0 min&lt;/td&gt;
&lt;td&gt;2.2 to 11.7 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App Router, Sep 19 onward&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;1.9 min&lt;/td&gt;
&lt;td&gt;2.0 min&lt;/td&gt;
&lt;td&gt;1.5 to 3.3 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Roughly a 70% cut in median build time. The window between those two rows is its own data point: four consecutive production builds at 27.1, 41.3, 44.8, and 46.2 minutes, every one of them cancelled or errored.&lt;/p&gt;

&lt;p&gt;Read that table carefully before quoting it. The Pages Router distribution is bimodal, clustering at 2 to 3 minutes and again at 6 to 8 minutes, which is almost certainly build cache hits against misses, so the median is the honest figure and the mean is not. The App Router sample is six builds over two days. And most of the gain comes from the ISR trade in Problem 1 rather than from the App Router itself. "Next.js 16 made our builds faster" would be the wrong conclusion to draw.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sitemap held.&lt;/strong&gt; 1,696 URLs live, 1,263 of them CMS-driven. That second number is what &lt;code&gt;additionalPaths&lt;/code&gt; carries and what would otherwise have disappeared.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runtime did not change, and we are not going to pretend it did.&lt;/strong&gt; Vercel keeps old deployments reachable at their unique URLs, so we measured the last Pages Router production deploy against current production on the same four routes. TTFB came back at 78 to 116 ms on the new build and 72 to 260 ms on the old. That is noise. Both serve CDN-cached HTML, so there was never a mechanism for the App Router to make it faster. Anyone promising you a speed win from this migration is selling something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ISR trade turned out to be free.&lt;/strong&gt; The stated risk in Problem 1 was that the first visitor to a cold slug pays for the render. We pulled two rarely-visited Rundown posts and got &lt;code&gt;x-vercel-cache: HIT&lt;/code&gt; and &lt;code&gt;STALE&lt;/code&gt; at 82 to 134 ms. Stale-while-revalidate serves the cached copy and regenerates behind it, so in practice nobody waits.&lt;/p&gt;

&lt;p&gt;What we cannot show yet is the measurement that matters most: Search Console impressions, clicks, and coverage before against after. That needs a few weeks of post-deploy data. If the sitemap work did its job, the correct result there is that nothing happens at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to take from this
&lt;/h2&gt;

&lt;p&gt;If you are moving a large CMS-backed site to the App Router:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Budget your build.&lt;/strong&gt; Count how many pages &lt;code&gt;generateStaticParams&lt;/code&gt; will prerender and multiply by your CMS round-trip. If that number approaches your platform's build cap, skip the prerender and lean on ISR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your sitemap comes from the route manifest, not from reality.&lt;/strong&gt; The moment you stop prerendering, check what &lt;code&gt;next-sitemap&lt;/code&gt; actually emitted. Query your CMS for &lt;code&gt;additionalPaths&lt;/code&gt; and fail the build loudly if it comes back empty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If the production build hangs at compile, try &lt;code&gt;--webpack&lt;/code&gt;.&lt;/strong&gt; Keep Turbopack for &lt;code&gt;next dev&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep &lt;code&gt;cookies()&lt;/code&gt; and &lt;code&gt;draftMode()&lt;/code&gt; out of cacheable routes.&lt;/strong&gt; Preview belongs on its own &lt;code&gt;force-dynamic&lt;/code&gt; route.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assume &lt;code&gt;next-seo&lt;/code&gt; is doing nothing.&lt;/strong&gt; Alias it to a no-op so you can migrate incrementally, then verify with a crawl rather than a spot check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diff the crawl, not the vibes.&lt;/strong&gt; Before, preview, after.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The site is live on Next.js 16 with the App Router, builds in minutes, and did not lose a URL. The content still lives in Cosmic, which is the part that made the sitemap fix possible: when your content model is queryable over an API, "list every URL this site should have" is a question you can answer at build time, independent of whatever your framework decided to prerender.&lt;/p&gt;

&lt;p&gt;If you want to see the setup, &lt;a href="https://www.cosmicjs.com/docs/frameworks#next-js" rel="noopener noreferrer"&gt;the Cosmic Next.js docs&lt;/a&gt; cover the SDK patterns used here, and &lt;a href="https://www.cosmicjs.com/changelog/live-preview-see-drafts-on-your-real-site" rel="noopener noreferrer"&gt;live preview&lt;/a&gt; is the feature behind the preview route.&lt;/p&gt;

&lt;p&gt;Cosmic is an AI-powered headless CMS with a REST API, TypeScript SDK, and AI agents. &lt;a href="https://app.cosmicjs.com/signup?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=nextjs16-app-router-migration" rel="noopener noreferrer"&gt;Start free, no credit card required&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>seo</category>
      <category>react</category>
    </item>
    <item>
      <title>Automating Editorial Triage: Routing Drafts Across 13 Properties with Cosmic and TypeSafe</title>
      <dc:creator>Tony Spiro</dc:creator>
      <pubDate>Mon, 21 Sep 2026 15:09:31 +0000</pubDate>
      <link>https://dev.to/tonyspiro/automating-editorial-triage-routing-drafts-across-13-properties-with-cosmic-and-typesafe-2mac</link>
      <guid>https://dev.to/tonyspiro/automating-editorial-triage-routing-drafts-across-13-properties-with-cosmic-and-typesafe-2mac</guid>
      <description>&lt;p&gt;Draft-only publishing is the right default for any editorial team large enough that no single person sees everything. Nothing reaches the public until an editor approves it. The policy is easy to adopt, and it creates a new problem on day one: every draft now needs a person, and people read in the order things arrive rather than the order that matters.&lt;/p&gt;

&lt;p&gt;Drafts can arrive from everywhere. Staff writers, section editors, freelancers filing against a brief, contributors who write for you twice a year, syndicated copy, and increasingly AI tools drafting alongside the team. The volume problem is the same whatever the source, and it shows up long before anyone adopts an agent.&lt;/p&gt;

&lt;p&gt;For one team running multiple web properties, that queue stops being readable within a week. Our &lt;a href="https://www.cosmicjs.com/blog/content-governance-at-scale" rel="noopener noreferrer"&gt;content governance at scale&lt;/a&gt; piece argued the policy. This one builds the mechanism that makes the policy survive volume.&lt;/p&gt;

&lt;p&gt;The approach: Cosmic holds the content, the draft state and the roles. A classification model decides which drafts a human actually has to open, and in what order. Below is the architecture, working code against both TypeScript SDKs, and the accuracy we measured on a real corpus, including the parts that did not work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Jev does, and what it does not do
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.typesafe.ai/introduction" rel="noopener noreferrer"&gt;TypeSafe&lt;/a&gt; ships a model called Jev that answers typed questions about a block of text. You send content as &lt;code&gt;state&lt;/code&gt; along with a map of questions, and you get structured answers back. There are three question types, documented on their &lt;a href="https://docs.typesafe.ai/api" rel="noopener noreferrer"&gt;API reference&lt;/a&gt; (read September 20, 2026):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Noul&lt;/strong&gt; returns the probability that a yes/no question is yes, as a number from 0 to 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choice&lt;/strong&gt; picks one option from a set you define, and returns the full probability distribution plus a &lt;code&gt;confidence&lt;/code&gt; value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score&lt;/strong&gt; rates content against an ordered rubric you define, from two to ten levels, and also returns &lt;code&gt;confidence&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Jev generates no prose and writes no code. It classifies and scores. That constraint is what makes it useful here, because editorial triage is mostly a classification job: decide what each draft is, then decide who needs to read it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;A draft is created in Cosmic by a staff writer, a freelancer filing against a brief, an occasional contributor, a syndication feed, or an agent. The pipeline does not care which.&lt;/li&gt;
&lt;li&gt;A webhook fires to your route handler.&lt;/li&gt;
&lt;li&gt;The handler sends the draft body to Jev with every question attached to a single request.&lt;/li&gt;
&lt;li&gt;Confidence decides what happens next: high-confidence answers write straight back to the object, low-confidence answers route to a human.&lt;/li&gt;
&lt;li&gt;The editor opens a queue that is already sorted, categorised and flagged.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The draft never publishes itself. Everything below only decides &lt;strong&gt;who looks at it and when&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: write the questions, and take criteria seriously
&lt;/h2&gt;

&lt;p&gt;This is the part that decides whether the whole thing works. Every question type accepts an optional or required &lt;code&gt;criteria&lt;/code&gt; field, and the quality of your criteria matters more than the wording of your instructions.&lt;/p&gt;

&lt;p&gt;Here is the question set we ran, targeting a fictional media group with thirteen consumer titles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;the draft body&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jev-latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"questions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"property"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"choice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"instructions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Which of our titles should publish this draft?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"criteria"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"switchback"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cycling: road, gravel, commuting, bike gear and maintenance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"field_and_forge"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Outdoor gear: hiking, camping, trail running, packs and footwear"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"the_marrow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Food: recipes, technique, restaurants, ingredient deep dives"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"marrow_dispatch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The Marrow's newsletter: short, single-subject, first-person food writing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ledger_lane"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Personal finance: saving, borrowing, tax, consumer money decisions"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"voice_fit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"instructions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rate how well this matches our editorial voice."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"criteria"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"Promotional or hyped. Superlatives without evidence, marketing register, reads like copy written to sell."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"Flat or generic. Accurate but characterless, could have come from any publication."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"Plain, specific and first-hand. Concrete detail, measured claims, an identifiable point of view."&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"unverified_claim"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"noul"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"instructions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Does this draft state a factual claim without attributing it to a source?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"criteria"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Contains a statistic, price, comparative claim or third-party assertion with no named source."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"false"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Every factual claim is attributed, or the piece is explicitly first-hand or opinion."&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"choice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"instructions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What kind of piece is this?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"criteria"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"news_report"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Reports something that happened, with dates, named sources or quotes."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"how_to_guide"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Instructional. Steps, technique or maintenance the reader is meant to follow."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"product_review"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Assesses named products against criteria and reaches a verdict."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"opinion_column"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Argues a position in an identifiable writer's voice."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"roundup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A list of products, places or picks grouped by a theme."&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"undisclosed_promotion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"noul"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"instructions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Does this draft promote a product, brand or sponsor without disclosing the relationship?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"criteria"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Promotional intent with no visible disclosure of sponsorship, gifting or affiliate links."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"false"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Either no promotional intent, or the relationship is disclosed in the draft."&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"structural_completeness"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"instructions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rate how complete this draft is as a finished piece."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"criteria"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"Truncated or fragmentary. Ends mid-thought, or whole sections are missing."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"Drafted but unfinished. Complete thoughts, with visible gaps or placeholders."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"Complete. An opening, a body and an ending that lands."&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the shape, because it is easy to get wrong. &lt;code&gt;questions&lt;/code&gt; is a &lt;strong&gt;map keyed by ids you choose&lt;/strong&gt;, and your answers come back under those same keys. Choice &lt;code&gt;criteria&lt;/code&gt; is a map of option to rubric description. Score &lt;code&gt;criteria&lt;/code&gt; is an &lt;strong&gt;ordered array&lt;/strong&gt; of level descriptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The single most useful thing we learned:&lt;/strong&gt; rubrics transformed the score question. Running voice fit with instructions alone, the model returned values of 1.64, 1.58 and 1.95 on a three-level scale, at confidence between 0.37 and 0.45. Those numbers are unusable, and we were ready to drop the question. Adding a described level for every step, exactly as written above, moved the same question to correct answers at confidence 0.94 and above. Same model, same task, same inputs.&lt;/p&gt;

&lt;p&gt;If a question is behaving badly, write better criteria before you conclude the model cannot do the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: one call, every question
&lt;/h2&gt;

&lt;p&gt;TypeSafe's &lt;a href="https://docs.typesafe.ai/patterns/fan-out" rel="noopener noreferrer"&gt;speculative fan-out pattern&lt;/a&gt; documents that questions in a request are evaluated in parallel, so adding more of them does not typically add latency. Combined with output tokens being free on Jev, asking six questions about a draft costs close to what asking one costs.&lt;/p&gt;

&lt;p&gt;That changes how you design the question set. Ask everything you might want, including questions that only matter sometimes, and ignore the irrelevant answers in code.&lt;/p&gt;

&lt;p&gt;Here is the handler. Both SDKs are TypeScript, so this is one file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createBucketClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@cosmicjs/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;QUESTIONS&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./questions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// the question map from Step 1&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cosmic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createBucketClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;bucketSlug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COSMIC_BUCKET_SLUG&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;readKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COSMIC_READ_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;writeKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COSMIC_WRITE_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TYPESAFE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.typesafe.ai/v1/systemone&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;questions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TYPESAFE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TYPESAFE_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jev-latest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;questions&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`TypeSafe &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;objectId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cosmic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;objectId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;props&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id,title,slug,metadata&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;buildState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;QUESTIONS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeBack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Jev accepts 64k tokens per request across state and all questions,&lt;/span&gt;
&lt;span class="c1"&gt;// and 32k for state plus the longest single question. Long-form bodies&lt;/span&gt;
&lt;span class="c1"&gt;// need trimming before they go in.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;`TITLE: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;markdown_content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A response comes back keyed by your question ids, with a &lt;code&gt;usage&lt;/code&gt; object carrying &lt;code&gt;input_tokens&lt;/code&gt; and &lt;code&gt;output_tokens&lt;/code&gt;. TypeSafe also publishes &lt;a href="https://docs.typesafe.ai/sdk" rel="noopener noreferrer"&gt;client SDKs&lt;/a&gt; that handle retries automatically with their default retry policy. If you call the HTTP API with raw &lt;code&gt;fetch&lt;/code&gt; as above, add your own backoff for the documented &lt;a href="https://docs.typesafe.ai/models" rel="noopener noreferrer"&gt;429 rate limit responses&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: gate on confidence
&lt;/h2&gt;

&lt;p&gt;This is where classification becomes a workflow. Choice and Score answers carry a &lt;code&gt;confidence&lt;/code&gt; value derived from the probability distribution. Above your threshold, act automatically. Below it, hand the draft to a person.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GATES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;property&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;content_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;unverified_claim&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;needsHuman&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;GATES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;needsHuman&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`Property unclear: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;probabilities&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(([,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content_type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;GATES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;needsHuman&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Content type unclear: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content_type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unverified_claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;noul&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;GATES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unverified_claim&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;needsHuman&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Possible unsourced factual claim&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;needsHuman&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;property&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choice&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flagged reasons matter as much as the flag. An editor who sees "cycling 0.78, outdoor gear 0.21" knows exactly what decision is being asked of them, and can make it in two seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: write the answer back
&lt;/h2&gt;

&lt;p&gt;The result lands on the Cosmic object, so the queue an editor opens is already organised:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;writeBack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;needsHuman&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;property&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cosmic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;voice_score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;voice_fit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;triage_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;needsHuman&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;needs_review&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto_filed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;triage_notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;needsHuman&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; | &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;needsHuman&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;property&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The object stays in draft. Nothing here publishes anything, and the human approval step is untouched.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we measured
&lt;/h2&gt;

&lt;p&gt;We built a corpus of 18 drafts across 13 fictional properties and five content types, with defects deliberately seeded on known labels so there was something real to score against. Two of the thirteen properties were newsletter microsites that overlap their parent titles, included specifically to make routing harder. Every draft went through one request carrying the full question set: 18 calls, 102 individual judgments, all HTTP 200.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Property routing: 18 of 18 correct.&lt;/strong&gt; Twelve came back at confidence 0.95 or above, including both newsletter traps. The finance newsletter resolved to the newsletter over its parent title at 0.84 against 0.16. The food newsletter resolved at 0.98 against 0.02.&lt;/p&gt;

&lt;p&gt;With the gate at 0.80, &lt;strong&gt;16 of 18 auto-filed with zero incorrect auto-files&lt;/strong&gt;, and 2 routed to a human. Both of those were genuinely ambiguous:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A fell-running shoe review: cycling title 0.78, outdoor title 0.21.&lt;/li&gt;
&lt;li&gt;A festival food guide: events title 0.46, food title 0.39, food newsletter 0.15, confidence 0.41.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are exactly the two a human should see.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Voice fit: 18 of 18 correct&lt;/strong&gt;, confidence 0.71 or above throughout. An affiliate-spam draft scored 0.00 at confidence 1.00. This is the question that was unusable before rubrics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content type: 13 of 18 correct&lt;/strong&gt;, and every single disagreement arrived below 0.55 confidence. Set that gate at 0.60 and everything passing the gate was right. As a demonstration of confidence gating, this is cleaner than the routing result, because the model reliably flagged its own errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Undisclosed promotion: 12 of 12.&lt;/strong&gt; A sponsored hotel review that discloses the sponsorship in its first line scored 0.05. An undisclosed affiliate roundup scored 0.94. We dropped this question from the last batch to keep payloads small, so the sample is 12 drafts rather than 18.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it was weak
&lt;/h2&gt;

&lt;p&gt;Publishing only the wins would make this useless to you, so here are the three limitations we hit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confidence catches genre ambiguity and misses topical overlap.&lt;/strong&gt; We built a batch specifically to break routing, using drafts that legitimately belong to two titles. It hedged on two and answered confidently on four where a second title had a real claim. An electric-vehicle charger payback analysis, which is half personal finance, returned its energy title at confidence 1.00 with the finance title at exactly 0. Do not use these probabilities to find cross-posting candidates. They answer "which one" and not "how many."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The unsourced-claim question tracks claim density more than sourcing.&lt;/strong&gt; Across twelve clean drafts it ranged from 0.11 to 0.73. A pure-opinion column containing no factual claims at all scored 0.11, while a fully sourced council report scored 0.59. Separation still held: every seeded-defect draft landed between 0.88 and 0.99, every clean draft stayed at or below 0.73, and a 0.80 gate was correct on all 18. Gate on the separation you observe in your own corpus rather than trusting the absolute value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structural completeness was the least reliable.&lt;/strong&gt; It caught a deliberately truncated guide at 0.03 with confidence 0.97, and it returned confidence 0.00 twice on drafts that are complete and simply end on a short line. Two zero-confidence outputs in 18 is worth knowing before you gate anything important on it.&lt;/p&gt;

&lt;p&gt;TypeSafe publishes a &lt;a href="https://docs.typesafe.ai/model-jaggedness/jev-1.13" rel="noopener noreferrer"&gt;jaggedness page&lt;/a&gt; for the model, which is an honest signal from a vendor and a reminder to measure on your own content before trusting any question in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;Jev is priced at $0.042 per million input tokens with output tokens free (&lt;a href="https://docs.typesafe.ai/models" rel="noopener noreferrer"&gt;TypeSafe models&lt;/a&gt;, read September 20, 2026). Our run consumed 22,416 input tokens across 18 calls and returned 4,656 output tokens that were not billed.&lt;/p&gt;

&lt;p&gt;That is &lt;strong&gt;$0.00094 for the whole corpus&lt;/strong&gt;, roughly $0.000052 per draft, or about 19,000 classified drafts per dollar. Cost is not the constraint on this design. Treat these figures as an illustration from one small run rather than a benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it without webhooks
&lt;/h2&gt;

&lt;p&gt;Webhooks on Cosmic are a $99/month per-project add-on, or $199/month bundled with Localization, Revision History and Automatic Backups (&lt;a href="https://www.cosmicjs.com/pricing" rel="noopener noreferrer"&gt;Cosmic pricing&lt;/a&gt;, read September 20, 2026). You do not need them to try this.&lt;/p&gt;

&lt;p&gt;Poll for untriaged drafts on a schedule instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;objects&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cosmic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;articles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;metadata.triage_status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;props&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id,title,metadata&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;buildState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;QUESTIONS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeBack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same classification, same write-back, no add-on required. Move to webhooks when the latency starts to bother you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves the review queue
&lt;/h2&gt;

&lt;p&gt;The editor still approves everything. What changed is what they open: a queue where most drafts are already filed against the right property, scored for voice, and where the handful carrying a possible unsourced claim are at the top with the reason attached.&lt;/p&gt;

&lt;p&gt;For a team running five to twenty properties, that difference decides whether draft-only publishing is a policy you can hold or one you quietly abandon.&lt;/p&gt;

&lt;p&gt;Cosmic gives you the content model, the draft state, the roles and the API to build this. &lt;a href="https://app.cosmicjs.com/signup?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=typesafe-jev-triage" rel="noopener noreferrer"&gt;Start free&lt;/a&gt;, read the &lt;a href="https://www.cosmicjs.com/docs" rel="noopener noreferrer"&gt;API and SDK docs&lt;/a&gt;, or &lt;a href="https://calendly.com/tonyspiro/cosmic-intro" rel="noopener noreferrer"&gt;book a walkthrough&lt;/a&gt; if you want to talk through the architecture for your own properties.&lt;/p&gt;

&lt;p&gt;Teams running this across many properties at once are who the &lt;a href="https://www.cosmicjs.com/pricing" rel="noopener noreferrer"&gt;Cosmic Workspace plans&lt;/a&gt; are built for: several projects, one team, one bill.&lt;/p&gt;

&lt;p&gt;If you are connecting AI tools to your content more broadly, the &lt;a href="https://www.cosmicjs.com/mcp-server" rel="noopener noreferrer"&gt;Cosmic MCP server&lt;/a&gt; is the other half of this story.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.cosmicjs.com/blog/automating-editorial-triage-typesafe-cosmic" rel="noopener noreferrer"&gt;the Cosmic blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>Content Durability: How to Get Everything Out of Your CMS</title>
      <dc:creator>Tony Spiro</dc:creator>
      <pubDate>Mon, 31 Aug 2026 15:09:38 +0000</pubDate>
      <link>https://dev.to/tonyspiro/content-durability-how-to-get-everything-out-of-your-cms-2693</link>
      <guid>https://dev.to/tonyspiro/content-durability-how-to-get-everything-out-of-your-cms-2693</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.cosmicjs.com/blog/content-durability-cms-export-path" rel="noopener noreferrer"&gt;Cosmic blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every CMS decision is a bet on where your content will live for the next five years. The part teams skip when they make that bet is the exit: what happens if you need to move, and what shape the content arrives in when it gets there.&lt;/p&gt;

&lt;p&gt;Content durability is the property that makes that question boring. Your content is durable when you can retrieve all of it, in a structured format, on demand, without filing a support ticket, and rebuild it somewhere else. Most teams assume they have this and find out they do not on the week they need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backup, revision history, and export solve three different failures
&lt;/h2&gt;

&lt;p&gt;These get conflated constantly, and the confusion is expensive because each one covers a failure the others do not.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backup&lt;/strong&gt; is a point-in-time restore back into the same system. It covers "we broke production at 3pm."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revision history&lt;/strong&gt; is a per-object change log inside the system. It covers "someone rewrote the pricing paragraph and we want the old one."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export&lt;/strong&gt; is a complete, structured copy that leaves the system. It covers "we are migrating," "legal needs an archive," and "the vendor changed terms."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A backup does not help you leave. An export does not help you recover a paragraph from yesterday. If you only have one of the three, you have a gap, and it is usually the export.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "structured" actually has to mean
&lt;/h2&gt;

&lt;p&gt;An export is only useful if the thing you get back can be rebuilt without a human reading it. Six properties matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;JSON, not rendered pages.&lt;/strong&gt; Page HTML is a lossy projection of your content. Field boundaries, types, and relationships are gone the moment it renders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The schema, not just the values.&lt;/strong&gt; Field keys, types, select options, required rules, and validation are what let you recreate the model somewhere else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stable IDs.&lt;/strong&gt; If identifiers are regenerated on export, every relationship in your content becomes a guess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relationships as references.&lt;/strong&gt; A post that points at an author and three tags should export those pointers, not flatten them into strings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Media you can actually retrieve.&lt;/strong&gt; File references are worthless without reachable originals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything, not just what is live.&lt;/strong&gt; Drafts, scheduled items, and locale variants are content too.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Path 1: the dashboard export
&lt;/h2&gt;

&lt;p&gt;Every Cosmic Bucket can export its content as a JSON file from Bucket &amp;gt; Settings &amp;gt; Import / Export. The export includes Object types, Objects, Media, and folders, which covers the schema and the values in one file. It does not include team members, Object revisions, or backups. That scope is documented on the &lt;a href="https://www.cosmicjs.com/docs/dashboard/buckets" rel="noopener noreferrer"&gt;Buckets documentation page&lt;/a&gt;, and the same screen imports a JSON file back into a Bucket, which is how you clone an environment or seed a fresh one.&lt;/p&gt;

&lt;p&gt;This is the fastest way to answer the durability question for yourself right now. Run the export, open the file, and check that the six properties above are present. That takes about five minutes and tells you more than any vendor claim.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path 2: the API on a schedule
&lt;/h2&gt;

&lt;p&gt;A manual export is a snapshot. A scheduled export is an insurance policy. The &lt;a href="https://www.cosmicjs.com/docs" rel="noopener noreferrer"&gt;REST API&lt;/a&gt; and the &lt;a href="https://www.npmjs.com/package/@cosmicjs/sdk" rel="noopener noreferrer"&gt;TypeScript SDK&lt;/a&gt; let you write the same data to your own storage on a cron, so a current copy always lives somewhere you control.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createBucketClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@cosmicjs/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;writeFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cosmic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createBucketClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;bucketSlug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COSMIC_BUCKET_SLUG&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;readKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COSMIC_READ_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;exportType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;all&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pageSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;skip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;objects&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cosmic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;props&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id,slug,title,type,status,locale,metadata,created_at,modified_at&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;any&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;skip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;skip&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;skip&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`./export/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.json`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;exportType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blog-posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Exported &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; objects`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details make the difference between a real export and a partial one. &lt;code&gt;status('any')&lt;/code&gt; includes drafts, which a default read leaves out. Requesting &lt;code&gt;metadata&lt;/code&gt; explicitly keeps every custom field rather than the summary props.&lt;/p&gt;

&lt;p&gt;Commit the output to a private repository or push it to object storage, and you get version history on your content for free. Once your object counts run into the thousands, switch the loop to cursor pagination with &lt;code&gt;after&lt;/code&gt;, which the API supports and which is more efficient than paging with large &lt;code&gt;skip&lt;/code&gt; offsets.&lt;/p&gt;

&lt;p&gt;A read key is enough for this job. An export process never needs write access, and keeping the two keys separate is the cheapest safety measure available.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path 3: the CLI
&lt;/h2&gt;

&lt;p&gt;For scripting against a terminal rather than an application, the &lt;a href="https://www.cosmicjs.com/docs/cli" rel="noopener noreferrer"&gt;Cosmic CLI&lt;/a&gt; reads objects, types, and media and can emit JSON for piping into other tools. It is the shortest route to a one-off dump inside a shell script or a CI job, without standing up a Node project first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part people forget: media
&lt;/h2&gt;

&lt;p&gt;Content exports usually succeed. Media migrations are where projects stall, because a JSON file full of CDN URLs is not the same thing as owning the files.&lt;/p&gt;

&lt;p&gt;Build the download step into your export job: walk the media references, fetch each original, and store it alongside the JSON. Do that once and your archive is genuinely self-contained.&lt;/p&gt;

&lt;p&gt;Two behaviors are worth knowing while you plan this. Replacing a media file keeps the same URL and id, so every reference in your content stays valid after the swap. Deleting media purges it from the CDN and does not update the references pointing at it, so a delete can leave broken links behind in objects you have forgotten about. Audit media before a migration, not after.&lt;/p&gt;

&lt;p&gt;If you are relying on transformations rather than storing multiple renditions, the derivative sizes are generated at request time from the original. Keeping the originals is what preserves your ability to regenerate everything later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backups and revisions, since export does not cover them
&lt;/h2&gt;

&lt;p&gt;Because the JSON export explicitly excludes revisions and backups, treat those as separate controls, and budget for them. Automatic Backups and Revision History are both add-ons on Cosmic, $99/month each, or $199/month for the bundle that also covers Webhooks and Localization. With Automatic Backups enabled, a backup runs daily, and you can take a snapshot on demand, download it, or restore from it. Revision history is tracked per object.&lt;/p&gt;

&lt;p&gt;The practical setup for most teams: backups on for recovery, revisions for editorial mistakes, and a scheduled JSON export for durability. Three controls, three failures, no overlap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the drill once a quarter
&lt;/h2&gt;

&lt;p&gt;An export you have never restored is a hypothesis. Turn it into a fact:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run a full export, dashboard or API, whichever your process uses.&lt;/li&gt;
&lt;li&gt;Download every referenced media original into the same archive.&lt;/li&gt;
&lt;li&gt;Import the JSON into a clean Bucket, or stand up a local database from it.&lt;/li&gt;
&lt;li&gt;Point a staging build at the restored data and load ten pages, including one with relationships and one with media.&lt;/li&gt;
&lt;li&gt;Write down what broke and how long it took.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The acceptance test is simple: could a developer who has never seen your setup rebuild a working staging site from the archive alone, in a day, with no access to the original account? If the answer is no, you have found the gap while it is cheap to fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seven questions to ask any CMS before you sign
&lt;/h2&gt;

&lt;p&gt;Bring these to the evaluation call and write down the answers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Can I export all content myself, without contacting support?&lt;/li&gt;
&lt;li&gt;Is the export structured JSON, and does it include the schema as well as the values?&lt;/li&gt;
&lt;li&gt;Are object IDs stable across export and import?&lt;/li&gt;
&lt;li&gt;Do relationships export as references I can resolve?&lt;/li&gt;
&lt;li&gt;Are drafts, scheduled content, and locale variants included?&lt;/li&gt;
&lt;li&gt;Can I retrieve every media original programmatically?&lt;/li&gt;
&lt;li&gt;Can the whole thing run on a schedule through the API?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A vendor that answers all seven cleanly is one you can leave, which is exactly why you probably will not need to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Cosmic stands
&lt;/h2&gt;

&lt;p&gt;Content in Cosmic is stored as structured Objects with a defined content model, reachable through the REST API and the TypeScript SDK, with a full JSON import and export in the dashboard and a CLI for scripting. Nothing about the format requires our software to read it. That is the standard worth holding every &lt;a href="https://www.cosmicjs.com/headless-cms" rel="noopener noreferrer"&gt;headless CMS&lt;/a&gt; to, including this one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://app.cosmicjs.com/signup?utm_source=dev.to&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=blog-content&amp;amp;utm_content=content-durability-cms-export-path"&gt;Start free with a Cosmic account&lt;/a&gt;, no credit card required.&lt;/p&gt;

</description>
      <category>cms</category>
      <category>api</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>When Agents Outnumber Editors</title>
      <dc:creator>Tony Spiro</dc:creator>
      <pubDate>Mon, 24 Aug 2026 22:39:53 +0000</pubDate>
      <link>https://dev.to/tonyspiro/when-agents-outnumber-editors-79p</link>
      <guid>https://dev.to/tonyspiro/when-agents-outnumber-editors-79p</guid>
      <description>&lt;p&gt;There is a ratio inside every content team that nobody tracks: how many things get written by people versus how many get written by software. For most teams that number was zero for a long time. Then it was a rounding error. Now, on a lot of teams, it has quietly crossed one to one.&lt;/p&gt;

&lt;p&gt;Once agents outnumber editors, the shape of the job changes. Drafts become cheap and abundant. The scarce resource is the judgment required to decide what deserves to ship. If your workflow was built around the old bottleneck, it will break in a specific and predictable way: a queue of plausible-looking drafts nobody has time to verify.&lt;/p&gt;

&lt;p&gt;The mechanics of what an agent is allowed to touch are a separate subject, and we covered those in &lt;a href="https://www.cosmicjs.com/blog/ai-agent-write-access-cms-controls" rel="noopener noreferrer"&gt;AI Agent Write Access: The 4 Real Controls You Get With Your CMS&lt;/a&gt;, including the two controls we do not ship yet. This post is about what happens upstream of any of those settings: how the daily work of a content team reorganizes itself once the ratio flips, and which four operating habits have to change with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ratio flips faster than the process does
&lt;/h2&gt;

&lt;p&gt;A two-person content team with four agents running scheduled jobs can produce more drafts in a week than the two humans can carefully read. Reading a 1,500-word post closely, checking its claims against primary sources, and verifying its links takes real time. Generating it takes minutes.&lt;/p&gt;

&lt;p&gt;So the throughput ceiling moves from writing to reviewing, and every process decision should follow that move. The goal is to make review fast and to make bad output structurally hard to publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shift 1: scope becomes a commissioning decision
&lt;/h2&gt;

&lt;p&gt;The most common mistake is giving one agent broad access to everything and hoping the prompt holds. Prompts drift. Keys do not.&lt;/p&gt;

&lt;p&gt;When you spin up a new agent, treat its scope the way you would treat a job description rather than a configuration screen you fill in afterward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write down the object types each agent is expected to touch, and put that list in its instructions. A social copy agent has no reason to touch your pricing page object, and it should be told so explicitly.&lt;/li&gt;
&lt;li&gt;Default every agent to draft output. Publishing should be a separate, deliberate action taken by someone accountable for it.&lt;/li&gt;
&lt;li&gt;Separate read keys from write keys in your environment. An agent that only summarizes traffic never needs a write key at all.&lt;/li&gt;
&lt;li&gt;Keep destructive operations off the table unless a human explicitly asks for them in that session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Be clear with yourself about which of these are enforced and which are conventions. In Cosmic today, the enforced boundary is the key an agent holds: read-only or read-write across the bucket. There is no per-object-type permission setting, so "this agent only touches blog posts" is an instruction you give and a habit you audit, not a rule the platform enforces for you. That is exactly why the read-key/write-key split matters so much, and why draft-by-default carries real weight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shift 2: review becomes the throughput ceiling
&lt;/h2&gt;

&lt;p&gt;Draft status is the cheapest safety mechanism you have. It costs nothing and it catches everything. The habit that has to change is calendar-level: review is now the work you schedule your week around, rather than a step you tack onto the end of someone else's.&lt;/p&gt;

&lt;p&gt;That means the review queue has to be a real surface your team sees without remembering to check the dashboard. Pull it with the &lt;a href="https://www.npmjs.com/package/@cosmicjs/sdk" rel="noopener noreferrer"&gt;TypeScript SDK&lt;/a&gt; and render it wherever your team already works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createBucketClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@cosmicjs/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cosmic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createBucketClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;bucketSlug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COSMIC_BUCKET_SLUG&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;readKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COSMIC_READ_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cosmic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blog-posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;props&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id,title,slug,status,created_at,metadata.author&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;any&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-created_at&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reviewQueue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;recent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Approval is then an explicit write, with a key that only your review surface holds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createBucketClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@cosmicjs/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cosmic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createBucketClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;bucketSlug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COSMIC_BUCKET_SLUG&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;readKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COSMIC_READ_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;writeKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COSMIC_WRITE_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cosmic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;objectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;published&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;last_updated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter more than they look. First, read the object back after a write and confirm the field actually stored the value you sent. A write that reports success while dropping a field is the kind of bug that ships a post with an empty meta description. Second, use explicit object IDs for relationship fields such as tags and category. Slugs can collide across object types and resolve to the wrong record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shift 3: attribution becomes a weekly number
&lt;/h2&gt;

&lt;p&gt;When five agents and two humans all touch the same bucket, "who wrote this" turns into an operational question you have to answer quickly. You need it to debug a bad claim, to retire an underperforming agent, and to answer the inevitable question about how much of the site is machine-written.&lt;/p&gt;

&lt;p&gt;Cosmic Insights attributes traffic by the actor who created the underlying object, splitting results across human users, agents, and automations. That answers a question most teams cannot answer at all: is the agent-authored content earning attention, or just filling the index?&lt;/p&gt;

&lt;p&gt;Run that report before you scale an agent up, and put it on the same cadence as your traffic review. An agent producing thirty posts a month that collectively earn less traffic than three human posts is a cost, and the fix is usually narrower scope rather than more volume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shift 4: the content model does the editing you no longer have time for
&lt;/h2&gt;

&lt;p&gt;The content model is the only guardrail that applies to every writer equally, human or machine. A required field is required for everyone. A select field with five options cannot be answered with a sixth. Every rule you move into the model is a review comment you never have to write again.&lt;/p&gt;

&lt;p&gt;Things worth encoding in the model rather than in a prompt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Required SEO fields.&lt;/strong&gt; If &lt;code&gt;seo_title&lt;/code&gt; and &lt;code&gt;seo_description&lt;/code&gt; are required, no agent can ship a post without them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Character limits.&lt;/strong&gt; A &lt;code&gt;maxlength&lt;/code&gt; on the meta description enforces the limit at write time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Select options in place of free text.&lt;/strong&gt; Category, content type, and funnel stage should be closed sets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A verification field.&lt;/strong&gt; For anything that cites a third party, a required &lt;code&gt;last_verified&lt;/code&gt; date makes staleness visible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Required relationships.&lt;/strong&gt; An author reference on every post means attribution is always captured.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these turns a review comment you would otherwise write by hand into a validation error the agent has to resolve before the object saves. Given that write access is bucket-wide rather than scoped per type, the model is doing more enforcement work than most teams realize.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure modes to watch for
&lt;/h2&gt;

&lt;p&gt;Four patterns show up repeatedly once agent volume rises:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Near-duplicate content.&lt;/strong&gt; Two agents, or the same agent on two runs, produce posts that target the same query. They then compete with each other in search. Search your own bucket before commissioning anything new.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidently stale third-party claims.&lt;/strong&gt; An agent reuses a competitor price or feature limit from an older draft. The rule that fixes this is simple: any claim about another product must be re-verified against that vendor's live page in the same run that writes the sentence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invented specificity.&lt;/strong&gt; Round numbers, benchmark figures, and customer counts that no source supports. Require a link next to every number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drift from the house voice.&lt;/strong&gt; Individually fine, collectively recognizable as machine output. Keep an explicit list of banned constructions and check drafts against it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A fifth one is worth naming because it caught us while writing this post: an agent describing your own product's capabilities from memory instead of from current documentation. The first version of this article claimed Cosmic could scope an agent's write access to a single object type. It cannot. Apply the same verification rule to your own product that you apply to competitors.&lt;/p&gt;

&lt;p&gt;Each of these is an argument for putting the gate before publication rather than after a correction.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does to headcount
&lt;/h2&gt;

&lt;p&gt;The interesting outcome is that the team does not grow the way you would expect. Output rises while headcount stays flat, and the roles that remain shift toward editing, verification, and deciding what to commission.&lt;/p&gt;

&lt;p&gt;Customers describe the same pattern when a CMS stops requiring a developer for routine changes. As Maximilian Wuhr, Co-Founder at FINN, put it: "Cosmic is: us never having to ask a developer to change anything on the backend of our website."&lt;/p&gt;

&lt;p&gt;If you do add a reviewer, the cost is predictable. Cosmic plans include a set number of team members (Free includes 2, Builder 3, Team 5, Business 10), and additional users are $29 per user per month. Current plan details are on the &lt;a href="https://www.cosmicjs.com/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;If you are heading toward this ratio, the sequence that works is: tighten the content model first, make draft the default second, build the review queue third, and add agents fourth.&lt;/p&gt;

&lt;p&gt;Cosmic gives you the pieces for all four. The &lt;a href="https://www.cosmicjs.com/headless-cms" rel="noopener noreferrer"&gt;headless CMS&lt;/a&gt; provides the content model and validation, the REST API and TypeScript SDK provide the review surface, the &lt;a href="https://www.cosmicjs.com/mcp-server" rel="noopener noreferrer"&gt;MCP server&lt;/a&gt; connects Claude, Cursor, or any MCP client directly to your content, and Insights tells you whether any of it is working. If you want the specific permission controls behind all of this, &lt;a href="https://www.cosmicjs.com/blog/ai-agent-write-access-cms-controls" rel="noopener noreferrer"&gt;we documented each one and how to verify it&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://app.cosmicjs.com/signup?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=blog-content&amp;amp;utm_content=when-agents-outnumber-editors-body" rel="noopener noreferrer"&gt;Start free with a Cosmic account&lt;/a&gt;, no credit card required.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.cosmicjs.com/blog/when-agents-outnumber-editors" rel="noopener noreferrer"&gt;cosmicjs.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cms</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>The Content Flywheel: How We Run AI Agents From Publish to Insights and Back</title>
      <dc:creator>Tony Spiro</dc:creator>
      <pubDate>Fri, 21 Aug 2026 15:46:32 +0000</pubDate>
      <link>https://dev.to/tonyspiro/the-content-flywheel-how-we-run-ai-agents-from-publish-to-insights-and-back-3m64</link>
      <guid>https://dev.to/tonyspiro/the-content-flywheel-how-we-run-ai-agents-from-publish-to-insights-and-back-3m64</guid>
      <description>&lt;p&gt;Most AI content tooling stops at the draft. You prompt a model, it produces something, a human edits it, and the loop ends there. Whatever happens to that content after publish never makes it back to the thing that wrote it.&lt;/p&gt;

&lt;p&gt;At Cosmic, we've completed the loop. Our agents publish into Cosmic, &lt;a href="https://www.cosmicjs.com/insights" rel="noopener noreferrer"&gt;Cosmic Insights&lt;/a&gt; measures what the content does, and the same agents read that data on a schedule and decide what to write, fix, or retire. That closed loop is what we call the content flywheel, and we run it on cosmicjs.com every day.&lt;/p&gt;

&lt;p&gt;Here is how it actually works, with real numbers from the project that powers content on this website.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four stages
&lt;/h2&gt;

&lt;p&gt;A content flywheel needs four things wired together. Miss one and it stops being a loop.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A content model an agent can write to.&lt;/strong&gt; Structured, queryable fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An agent with real capabilities.&lt;/strong&gt; Read the CMS, write to the CMS, browse the web, post to Slack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measurement attached to the content.&lt;/strong&gt; Traffic attributed back to the object itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A scheduled goal that closes the loop.&lt;/strong&gt; Something that wakes up, reads the data, and acts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cosmic gives you all four in one place, which is the entire reason the loop is short enough to be useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 1: The content model
&lt;/h2&gt;

&lt;p&gt;Our blog post type has the fields you would expect, plus the ones that make measurement possible: &lt;code&gt;seo_title&lt;/code&gt;, &lt;code&gt;seo_description&lt;/code&gt;, &lt;code&gt;teaser&lt;/code&gt;, &lt;code&gt;published_date&lt;/code&gt;, &lt;code&gt;last_updated&lt;/code&gt;, and relationship fields for author, category, and tags.&lt;/p&gt;

&lt;p&gt;That structure matters more than it sounds. Because &lt;code&gt;last_updated&lt;/code&gt; is a real date field and &lt;code&gt;tags&lt;/code&gt; is a real relationship, an agent can ask questions like "which posts tagged AI Agents have not been touched in 90 days" and get an answer it can act on. A single freeform body field leaves that question unanswerable.&lt;/p&gt;

&lt;p&gt;Agents read and write this model through the same REST API and TypeScript SDK your app uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createBucketClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@cosmicjs/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cosmic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createBucketClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;bucketSlug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COSMIC_BUCKET_SLUG&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;readKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COSMIC_READ_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Find posts in a topic cluster that are going stale.&lt;/span&gt;
&lt;span class="c1"&gt;// Query relationship fields by object ID, not slug: slugs can&lt;/span&gt;
&lt;span class="c1"&gt;// collide across object types and resolve to the wrong object.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;objects&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cosmic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blog-posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;metadata.tags&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;6a21aa1ed66dd9646b9e028a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// "AI Agents" tag object ID&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;metadata.last_updated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$lt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-05-20&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;props&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id,title,slug,metadata.last_updated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent uses the same query. There is no separate agent API to learn and no second copy of your content to keep in sync.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 2: The agents
&lt;/h2&gt;

&lt;p&gt;An agent in Cosmic is a team member you configure. You grant capabilities: read content, write content, browse the web, call external APIs, send messages to Slack or Telegram, read analytics, work on a connected repository. What the agent does is decided by the capabilities you give it.&lt;/p&gt;

&lt;p&gt;Ours are split by job, the same way a content team is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A content lead agent that researches, writes, and publishes into the bucket.&lt;/li&gt;
&lt;li&gt;A social coordinator agent that pulls the day's published posts and writes platform-specific copy.&lt;/li&gt;
&lt;li&gt;A growth agent that reads Insights and reports to the team in Slack.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They hand work to each other. The social agent asks the content agent what shipped in the last 24 hours, gets back titles, slugs, and categories, and writes from that instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 3: Measurement that points back at the content
&lt;/h2&gt;

&lt;p&gt;This is the stage most teams skip, and it is the one that makes the loop real.&lt;/p&gt;

&lt;p&gt;Cosmic Insights tracks pageviews, visitors, sessions, bounce rate, custom events, and revenue. The part that matters for a flywheel is object attribution. Add one meta tag to your page and traffic rolls up by Cosmic object alongside the URL path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"cosmic-context"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;'{"object_id":"6a873688f234aa885b778173","object_type":"blog-posts"}'&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the agent that wrote the post can ask how that exact object performed. Custom events fill in the rest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cosmicInsights&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signup_cta_click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;object_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;6a873688f234aa885b778173&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;object_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blog-posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On cosmicjs.com, here is what our event data looked like over the 30 days ending August 20, 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Events&lt;/th&gt;
&lt;th&gt;Visitors&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;signup_started&lt;/td&gt;
&lt;td&gt;751&lt;/td&gt;
&lt;td&gt;645&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;signup_completed&lt;/td&gt;
&lt;td&gt;350&lt;/td&gt;
&lt;td&gt;335&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cta_click&lt;/td&gt;
&lt;td&gt;314&lt;/td&gt;
&lt;td&gt;247&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;signup_cta_click&lt;/td&gt;
&lt;td&gt;265&lt;/td&gt;
&lt;td&gt;223&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lesson_view&lt;/td&gt;
&lt;td&gt;243&lt;/td&gt;
&lt;td&gt;97&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A 46.6% completion rate from signup start to finish is a number we can hold a strategy against. Google organic sent 19,664 events from 16,838 visitors in that same window, which tells us which half of the funnel deserves attention. These are rolling-window figures, so they move; the point is that an agent can pull them on demand rather than guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 4: The scheduled goal that closes it
&lt;/h2&gt;

&lt;p&gt;A capability without a cadence is still a manual process. In Cosmic you give an agent a goal with a cron schedule, and it works through that goal in a bounded loop until it decides it is done.&lt;/p&gt;

&lt;p&gt;Our loop-closing goals look roughly like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Weekly, Tuesday morning:&lt;/strong&gt; read the last seven days of Insights by path and by custom event, compare against the prior seven, and post the deltas to Slack with content recommendations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weekly:&lt;/strong&gt; scan competitor blogs and pricing pages, cite every claim with the URL fetched in that same run, and recommend counter-content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daily:&lt;/strong&gt; report which posts published in the last 24 hours so downstream agents can act on them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule we enforce on every one of these: every number in the output has to come from a tool call made in that same run. No remembered figures, no plausible-looking estimates. An agent that fabricates a metric poisons the flywheel faster than a slow one starves it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The week the flywheel caught something we missed
&lt;/h2&gt;

&lt;p&gt;Here is the concrete example, and it is not a flattering one.&lt;/p&gt;

&lt;p&gt;Our highest-traffic blog cluster is a set of AI coding tool comparisons. They pull thousands of Google visitors a month. When we pulled &lt;code&gt;signup_cta_click&lt;/code&gt; broken down by page, none of those posts appeared in the list. Zero measured CTA clicks against our best organic traffic.&lt;/p&gt;

&lt;p&gt;The obvious conclusion was that the posts had no calls to action. So we read the objects back out of the CMS and checked. Every post had a mid-article signup block and a bottom CTA block, both rendering correctly on the live page.&lt;/p&gt;

&lt;p&gt;The actual problem was in the shared CTA blocks. Cosmic lets you define reusable rich-text blocks and reference them from any post with a token like &lt;code&gt;{{bottom-cta}}&lt;/code&gt;. Ours pointed at a bare signup URL with no UTM parameters and no click event bound to it. The clicks were happening. Nothing was recording them.&lt;/p&gt;

&lt;p&gt;One edit to two shared blocks instrumented every blog post in the bucket at once, because the blocks are referenced by every post that uses them. The blog channel now shows up in source attribution.&lt;/p&gt;

&lt;p&gt;The most valuable output here was diagnostic. The flywheel told us that a question we thought we had answered had never actually been measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we got wrong the first time
&lt;/h2&gt;

&lt;p&gt;Three honest lessons from running this for a while:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attribution gaps look like content failures.&lt;/strong&gt; Our first read of the data said the comparison posts were bad at converting. The real answer was that we could not see the conversions. Verify instrumentation before you rewrite content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the object back after every write.&lt;/strong&gt; We have had writes report success while silently dropping a field, usually on repeaters and relationship fields. Our agents now re-read every object after writing it and confirm the field actually stored. Use explicit object IDs for relationship fields, since slugs can collide across object types.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bounce rate is a weak signal on its own.&lt;/strong&gt; A developer who lands from Google, reads a comparison post, and leaves satisfied counts as a bounce. Pair it with custom events before you conclude anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building your own
&lt;/h2&gt;

&lt;p&gt;The shortest path to a working loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Model your content with real fields, especially dates and relationships. Agents can only act on structure they can query.&lt;/li&gt;
&lt;li&gt;Add the Insights script and a &lt;code&gt;cosmic-context&lt;/code&gt; meta tag to your content templates so traffic rolls up by object.&lt;/li&gt;
&lt;li&gt;Fire one custom event on your primary conversion action.&lt;/li&gt;
&lt;li&gt;Create an agent with content read and write capability, plus analytics access.&lt;/li&gt;
&lt;li&gt;Give it one scheduled goal that reads last week's data and reports what changed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Start with step five as a report-only goal. Let it tell you what it sees for a few weeks before you let it publish anything. Trust in the loop should be earned with evidence, and the loop is very good at producing evidence.&lt;/p&gt;

&lt;p&gt;You can read more about how agents are configured on the &lt;a href="https://www.cosmicjs.com/ai/agents" rel="noopener noreferrer"&gt;Cosmic AI agents page&lt;/a&gt;, or connect your own AI tools directly to your content model through the &lt;a href="https://www.cosmicjs.com/mcp-server" rel="noopener noreferrer"&gt;Cosmic MCP server&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Give your AI agents a content backend they can write to.&lt;/strong&gt; Structured, versioned content objects, a REST API and TypeScript SDK, and an MCP server your coding agent connects to directly. The Free plan includes 1 Bucket, 1,000 Objects, and 1 agent. No credit card required.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://app.cosmicjs.com/signup?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=blog-content&amp;amp;utm_content=devto-bottom-signup-cta" rel="noopener noreferrer"&gt;Start for free&lt;/a&gt; or &lt;a href="https://www.cosmicjs.com/ai?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=blog-content&amp;amp;utm_content=devto-bottom-ai-page" rel="noopener noreferrer"&gt;see Cosmic for AI teams&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cms</category>
      <category>webdev</category>
      <category>seo</category>
    </item>
    <item>
      <title>How to Migrate from Payload CMS to Cosmic</title>
      <dc:creator>Tony Spiro</dc:creator>
      <pubDate>Sat, 15 Aug 2026 19:34:05 +0000</pubDate>
      <link>https://dev.to/tonyspiro/how-to-migrate-from-payload-cms-to-cosmic-4p7c</link>
      <guid>https://dev.to/tonyspiro/how-to-migrate-from-payload-cms-to-cosmic-4p7c</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Last verified: August 14, 2026.&lt;/strong&gt; Every claim about Payload on this page was checked against Payload's own live pages on that date: &lt;a href="https://payloadcms.com/cloud-pricing" rel="noopener noreferrer"&gt;Payload Cloud pricing and status&lt;/a&gt;, &lt;a href="https://payloadcms.com/docs/getting-started/what-is-payload" rel="noopener noreferrer"&gt;What is Payload&lt;/a&gt;, and the &lt;a href="https://payloadcms.com/docs/local-api/overview" rel="noopener noreferrer"&gt;Local API docs&lt;/a&gt;. Cosmic pricing was checked against &lt;a href="https://www.cosmicjs.com/pricing" rel="noopener noreferrer"&gt;cosmicjs.com/pricing&lt;/a&gt;. Note that &lt;code&gt;payloadcms.com/pricing&lt;/code&gt; returned a 404 on this date; Cloud plan details now live on the &lt;code&gt;/cloud-pricing&lt;/code&gt; page.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What actually changed at Payload
&lt;/h2&gt;

&lt;p&gt;Payload has joined Figma. The announcement is &lt;a href="https://www.figma.com/blog/payload-joins-figma/" rel="noopener noreferrer"&gt;on the Figma blog&lt;/a&gt;, and it is linked from the top of every page on payloadcms.com.&lt;/p&gt;

&lt;p&gt;Two things follow from that, both stated by Payload directly on its Cloud page:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;New Payload Cloud deployments are paused.&lt;/strong&gt; In Payload's words: "Although deployment of new projects is currently paused, existing Cloud projects will continue running as normal."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Existing Cloud projects will eventually move.&lt;/strong&gt; From the same page's FAQ, answering "Will I need to migrate my project?": "Yes, eventually. There is no rush, but we are planning to build something better that you will be able to migrate to once it's available."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is what has &lt;em&gt;not&lt;/em&gt; changed. Payload the framework is still open source, still actively developed, and still self-hostable. Payload's own docs state that Payload remains a self-hosted solution, and that anywhere you can run a Next.js app, you can run Payload. If you self-host Payload today, nothing above forces your hand.&lt;/p&gt;

&lt;p&gt;The teams with a real decision to make are the ones who picked Payload Cloud because they wanted someone else to run the database, the file storage, and the deploys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you migrate at all?
&lt;/h2&gt;

&lt;p&gt;Three honest options. Pick based on your team, not on vendor news.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stay on Payload and self-host it.&lt;/strong&gt; The right call if your content model leans on Payload's code-first strengths: custom access control, hooks, field-level permissions, or an admin panel you have meaningfully customized in React. You will need to own a database, object storage, and a deploy target. Payload is a Next.js application, so this is familiar work for a Next.js team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wait for whatever Figma ships.&lt;/strong&gt; Payload says there is no rush and that a migration path to something new is planned. If your project is stable and you have infrastructure people, waiting costs you little.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Move to a managed API-first CMS.&lt;/strong&gt; The right call if the reason you chose Payload Cloud was that you did not want to run infrastructure, and you would rather not go back to running it. That is the path this guide covers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you give up moving to Cosmic
&lt;/h2&gt;

&lt;p&gt;Stating this plainly, because a migration guide that pretends there are no tradeoffs is not worth reading.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Local API goes away.&lt;/strong&gt; Payload's biggest technical advantage is that it runs in the same Node process as your app, so you can query the database directly from a React Server Component with no network hop. Cosmic is an HTTP API. For most content sites the difference disappears behind caching and static generation, but it is a genuine architectural change and you should know it going in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No GraphQL.&lt;/strong&gt; Payload exposes REST and GraphQL. Cosmic offers a REST API and a TypeScript SDK, and does not offer GraphQL. If your frontend is built on Payload's GraphQL endpoint, that query layer gets rewritten.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code-first config becomes dashboard-first modeling.&lt;/strong&gt; In Payload your collections live in version-controlled TypeScript. In Cosmic you define Object Types in the dashboard or over the API. Some teams consider that a downgrade in reviewability, and others consider it the point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom admin components.&lt;/strong&gt; React components you injected into the Payload admin panel do not carry over.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth and access control.&lt;/strong&gt; Payload ships user auth and granular access control as first-class features. If you used Payload as your application's auth layer and not only as a CMS, that responsibility moves elsewhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If several of those matter a lot to you, self-hosting Payload is probably the better answer, and you should stop reading here. No hard feelings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mapping Payload concepts to Cosmic
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Payload&lt;/th&gt;
&lt;th&gt;Cosmic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Collection&lt;/td&gt;
&lt;td&gt;Object Type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Document&lt;/td&gt;
&lt;td&gt;Object&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global&lt;/td&gt;
&lt;td&gt;Single Object (a type with one Object)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Field&lt;/td&gt;
&lt;td&gt;Metafield&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;slug&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Object slug&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upload collection&lt;/td&gt;
&lt;td&gt;Media library&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Relationship field&lt;/td&gt;
&lt;td&gt;Object metafield (&lt;code&gt;object&lt;/code&gt; / &lt;code&gt;objects&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Array field&lt;/td&gt;
&lt;td&gt;Repeater metafield&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blocks field&lt;/td&gt;
&lt;td&gt;Repeater, or rich text with Content Blocks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Group field&lt;/td&gt;
&lt;td&gt;Parent metafield group&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Draft / published&lt;/td&gt;
&lt;td&gt;Object status (&lt;code&gt;draft&lt;/code&gt; / &lt;code&gt;published&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Localization&lt;/td&gt;
&lt;td&gt;Locale variants on an Object Type&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The shapes line up closely enough that most migrations are a data-transform problem rather than a redesign.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: inventory what you actually have
&lt;/h2&gt;

&lt;p&gt;Before writing any script, list every collection and global, the document count in each, and which fields are genuinely used. Migrations balloon because teams port fields nobody has filled in for two years.&lt;/p&gt;

&lt;p&gt;Payload's Local API gives you counts quickly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getPayload&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@payload-config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPayload&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;collection&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;totalDocs&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;totalDocs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Write the output down. It is your migration checklist and your verification target at cutover.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: export from Payload
&lt;/h2&gt;

&lt;p&gt;Run this inside your Payload project so &lt;code&gt;@payload-config&lt;/code&gt; resolves. Two details matter: &lt;code&gt;pagination: false&lt;/code&gt; returns every document instead of the first page, and &lt;code&gt;depth: 0&lt;/code&gt; keeps relationships as raw IDs instead of expanding them into nested objects. You want raw IDs, because you are going to remap them yourself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getPayload&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@payload-config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPayload&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./payload-export&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Collections&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;collection&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;docs&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;overrideAccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.json`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`exported &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; from &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Globals&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nb"&gt;global&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;globals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findGlobal&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;global&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`global-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;global&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.json`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit that folder somewhere safe. Everything after this point is reversible as long as the export exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: model your content in Cosmic
&lt;/h2&gt;

&lt;p&gt;Create one Object Type per Payload collection. You can do it in the dashboard, or script it so the whole migration is repeatable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createBucketClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@cosmicjs/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cosmic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createBucketClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;bucketSlug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COSMIC_BUCKET_SLUG&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;readKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COSMIC_READ_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;writeKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COSMIC_WRITE_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cosmic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objectTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;singular&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Post&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;metafields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;excerpt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Excerpt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;textarea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;markdown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hero&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hero Image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;author&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Author&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;object_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;authors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Migrate types with no relationships first (authors, categories, tags), then the types that point at them. That ordering saves you a second reconciliation pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: move the media
&lt;/h2&gt;

&lt;p&gt;Payload upload documents store a filename and a URL. Pull each file and push it into the Cosmic media library, keeping a map from the old Payload ID to the new Cosmic media name so you can rewrite references later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;uploadDocs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;media&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cosmic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;media&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;media&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;originalname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payload-import&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;alt_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alt&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nx"&gt;idMap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;media&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you set &lt;code&gt;alt&lt;/code&gt; text in Payload, carry it across now. Alt text lives on the Cosmic media record itself, so every Object referencing that image inherits it, which is one less accessibility cleanup later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: convert rich text
&lt;/h2&gt;

&lt;p&gt;This is the step that surprises people, so budget real time for it.&lt;/p&gt;

&lt;p&gt;Payload stores rich text as a structured JSON tree, not as markdown or HTML. You cannot drop that JSON into a markdown metafield and expect it to render. You need a serializer that walks the tree and emits markdown or HTML.&lt;/p&gt;

&lt;p&gt;Payload's rich text documentation covers converting its editor state to other formats, and that is the tool to reach for. Two things to watch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Uploads embedded in rich text&lt;/strong&gt; become nodes referencing an upload ID. Rewrite those to the Cosmic media URLs from your &lt;code&gt;idMap&lt;/code&gt; in Step 4.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blocks embedded in rich text&lt;/strong&gt; need a destination. Either flatten them into markdown, or create a reusable Cosmic Content Block and reference it with a &lt;code&gt;{{block-name /}}&lt;/code&gt; token in a rich text metafield.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Convert one document, eyeball the output, then convert the rest. Do not batch-convert 2,000 documents on faith.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: import into Cosmic
&lt;/h2&gt;

&lt;p&gt;With media mapped and rich text serialized, the import itself is short. Keep a Payload-ID to Cosmic-ID map as you go so relationship fields can be resolved on a second pass.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createBucketClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@cosmicjs/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./payload-export/posts.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cosmic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createBucketClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;bucketSlug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COSMIC_BUCKET_SLUG&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;readKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COSMIC_READ_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;writeKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COSMIC_WRITE_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cosmic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;published&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;published&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;excerpt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;excerpt&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;toMarkdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;hero&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hero&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;idMap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hero&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;authorIdMap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Preserve the original &lt;code&gt;slug&lt;/code&gt; values. That single decision is what keeps your URLs, your rankings, and your inbound links intact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: rewire the frontend
&lt;/h2&gt;

&lt;p&gt;If your app is Next.js, this is the most mechanical part of the whole project. Payload Local API calls become Cosmic SDK calls.&lt;/p&gt;

&lt;p&gt;Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPayload&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;docs&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-publishedDate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createBucketClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@cosmicjs/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cosmic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createBucketClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;bucketSlug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COSMIC_BUCKET_SLUG&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;readKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COSMIC_READ_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;objects&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cosmic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;props&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;slug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;metadata&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-created_at&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;.props()&lt;/code&gt; to request only the fields the page renders. Payload's Local API had no network cost, so over-fetching was cheap. Over an HTTP API it is not, and trimming the payload is the single easiest performance win in the port.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: verify, then cut over
&lt;/h2&gt;

&lt;p&gt;Work through this before you flip DNS:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Counts match.&lt;/strong&gt; Compare every Object Type count against the inventory from Step 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spot-check the ugly documents.&lt;/strong&gt; Not the simple ones. The post with nested blocks, four images, and a relationship array.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every relationship resolves.&lt;/strong&gt; Query with &lt;code&gt;.depth(1)&lt;/code&gt; and confirm nothing comes back null.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slugs are identical&lt;/strong&gt; to production Payload for every public URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Media loads&lt;/strong&gt; on a real page render, not only in the dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redirects staged&lt;/strong&gt; for any URL that genuinely had to change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep Payload running&lt;/strong&gt; in parallel until the checks pass. There is no prize for deleting it early.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What this costs on Cosmic
&lt;/h2&gt;

&lt;p&gt;Verified against &lt;a href="https://www.cosmicjs.com/pricing" rel="noopener noreferrer"&gt;cosmicjs.com/pricing&lt;/a&gt; on August 14, 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Buckets&lt;/th&gt;
&lt;th&gt;Team members&lt;/th&gt;
&lt;th&gt;Objects&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;$0/mo&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Builder&lt;/td&gt;
&lt;td&gt;$49/mo&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team&lt;/td&gt;
&lt;td&gt;$299/mo&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;20,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business&lt;/td&gt;
&lt;td&gt;$499/mo&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;50,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Additional team members beyond a plan's included seats are $29/user/month.&lt;/p&gt;

&lt;p&gt;The practical move is to run the whole migration on the Free plan first. One Bucket and 1,000 Objects is enough to prove the export, the transform, and the import against real content before anyone approves a budget line.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is Payload CMS being shut down?&lt;/strong&gt;&lt;br&gt;
No, and nothing on this page should be read that way. Payload is open source and self-hostable, and Payload states that existing Cloud projects continue running as normal. What changed is that new Payload Cloud deployments are paused, and Payload says existing Cloud projects will eventually need to migrate to a future replacement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I keep my URLs?&lt;/strong&gt;&lt;br&gt;
Yes, as long as you carry the &lt;code&gt;slug&lt;/code&gt; field across unchanged. Cosmic Objects have a slug you control, so a one-to-one mapping is normal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Cosmic support GraphQL?&lt;/strong&gt;&lt;br&gt;
No. Cosmic provides a REST API and a TypeScript SDK. Payload does expose GraphQL, so if your frontend depends on it, plan on rewriting that data layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How long does a migration take?&lt;/strong&gt;&lt;br&gt;
It depends almost entirely on rich text and blocks. A handful of collections with plain fields is an afternoon. A heavily block-driven site with thousands of documents is a multi-week project, and the serializer is where the time goes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I run both during the transition?&lt;/strong&gt;&lt;br&gt;
Yes, and you should. Import into Cosmic, point a staging branch at it, verify against the Step 8 checklist, and only then cut over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;If you were on Payload Cloud for the managed hosting and you want to stay out of the infrastructure business, the cheapest way to evaluate Cosmic is to migrate one collection and look at the result.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://app.cosmicjs.com/signup?utm_source=dev.to&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=migrate-payload-cms-to-cosmic&amp;amp;utm_content=closing-cta"&gt;Create a free Bucket&lt;/a&gt; and run the export script above against your smallest collection. If you would rather walk through your content model with someone first, &lt;a href="https://calendly.com/tonyspiro/cosmic-intro" rel="noopener noreferrer"&gt;book time with our CEO Tony Spiro&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;More reading:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.cosmicjs.com/payload-alternative" rel="noopener noreferrer"&gt;Payload CMS Alternative&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cosmicjs.com/blog/payload-cms-vs-cosmic-which-headless-cms-is-right-for-you" rel="noopener noreferrer"&gt;Payload CMS vs Cosmic: Which Headless CMS Is Right for You?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cosmicjs.com/blog/payload-vs-strapi" rel="noopener noreferrer"&gt;Payload vs Strapi: Which Open-Source Headless CMS Should You Choose in 2026?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>headlesscms</category>
      <category>nextjs</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
