Pricing
Get a demoContinue with
  • Content-led Growth Agent
  • Performance Marketing Agent
  • Outbound Automation Agent
  • Cursor GTM
  • Cursor Agency
  • Invest
  • AI Search Visibility for Healthcare

© Metaflow AI, Inc. 2026

PRODUCTS

  • Agents
  • Content-led Growth
  • Performance Marketing
  • Outbound Automation
  • Flow

SOLUTIONS

  • AI Marketing Agent
  • GTM
  • SEO Automation
  • Bottom-Funnel Content
  • Google Ads Agents
  • Meta Ads Agents
  • GTM Workflow Playbook
  • Healthcare AI Search Visibility

CUSTOMERS

  • Guideflow
  • Hyring

BY ROLE

  • For Growth Marketers
  • For GTM Engineers
  • For Founders

RESOURCES

  • Blog
  • Guides
  • Technical SEO Guides
  • FAQ
  • Learning Center
  • Skills
  • Free Tools
  • Cursor GTM
  • Invest
  • Tutorials

COMPARISON GUIDES

  • Metaflow AI vs Claude
  • Metaflow AI vs AirOps
  • Metaflow AI vs n8n
  • Metaflow AI vs Dust.tt

GET STARTED

  • Plans & Pricing
  • Book a Demo

SUPPORT

  • Changelog
  • Help

COMPANY

  • About
  • Founder
  • Contact Us
  • Privacy Policy
  • Terms of Use
  • Cookie Policy
Metaflow AI, Inc2261 Market Street #10708San Francisco, CA 94114

Designed with ♥ by GrowthLane

Pricing
Get a demoContinue with
Cover Image for Sanity Programmatic Publishing: The MDP Gate Stack

Sanity Programmatic Publishing: The MDP Gate Stack

Ship Sanity blog drafts from markdown without Studio paste work. Metaflow's MDP Gate covers Portable Text conversion, hero image upload, and QA-gated Draft publish.

AI Marketing
byMetaflow TeamLast Updated on Jun 16, 2026
M
What sanity programmatic publishing means in 2026SERP consensus: what ranking pages already sayThe MDP Gate: Metaflow's three-stage publish stackPortable Text conversion: what blog-md-to-portable-text.mjs actually emitsImage upload and hero assets in the publish pathThe Draft flow: QA hard gates before createOrReplaceMDP vs alternatives: which Sanity publish path to pickFrequently Asked Questions

Sanity programmatic publishing writes blog documents to the Content Lake through the Sanity API. You turn markdown into Portable Text blocks. You attach hero images. You save posts as Drafts for Studio review. No more paste work in the visual editor.

According to Ahrefs research from April 2026, 74.2% of new webpages contain AI-generated content. Teams draft in markdown. Sanity schemas want Portable Text on the `body` field. Sanity programmatic publishing bridges that gap with API writes.

TL;DR

  • The MDP Gate stacks Markdown files, a Portable Text converter, and QA-gated Draft publish.
  • `blog-md-to-portable-text.mjs` emits blocks, tables, links, and `youtubeVideo` custom types.
  • `publish-from-files.mjs` enriches, runs hard QA gates, uploads heroes, then calls `createOrReplace`.
  • Draft-first publish keeps editors in control. Studio sets `Done` and `publishedAt` to ship.
  • Vendor bulk tools skip teardown. This page documents Metaflow's production pipeline.

What sanity programmatic publishing means in 2026

Programmatic publishing in a Sanity context means your pipeline assembles the post document. Not a human in Studio. You set title, slug, excerpt, SEO object, cover image reference, and a `body` array of Portable Text blocks. You authenticate with `SANITY_API_TOKEN`, call the client, and write.

Studio paste vs API createOrReplace

Manual workflow: copy markdown into Studio, fix formatting, upload a hero, set categories, save as Draft. Works for one post. Breaks at ten posts per month when every draft originates in chat or a files folder.

Programmatic workflow: a Node script loads `brief.json` plus `draft.md`, converts body markdown, runs QA, optionally generates a hero via image API, then upserts `post-{slug}` with `status: 'Draft'`. The content engineer reviews in Studio. Same outcome, zero paste labor.

Why Draft-first beats live publish

Metaflow's `publishBlogPost` sets `status: 'Draft'` and leaves `publishedAt` null. Automated pipelines should not bypass editorial review. Programmatic speed belongs in assembly, not in going live unseen. A Draft landing in Studio is the contract: machines prepare, humans approve.

Where programmatic fits content engineering

If your team already runs an information gain content framework and scores briefs before drafting, sanity programmatic publishing is the Systems layer. The script enforces gates and writes only when QA passes. Without it, you have great markdown files and a Studio queue nobody wants to populate.

Searcher needWhere we answer it
Define programmatic Sanity publishOpening + this section
Markdown to Portable Text pathMDP Gate + converter teardown
Image upload via APIHero asset section
Draft vs live publishDraft flow section
Pick a publish pathMDP vs alternatives table

SERP consensus: what ranking pages already say

The live SERP for sanity programmatic blog publishing clusters into three camps. Each is useful. Each is incomplete.

Portable Text serialization guides

Sanity's presenting Portable Text guide is the main read path. It lists `@portabletext/react` and `@portabletext/block-tools`. It covers join syntax for internal links. It is strong on rendering. It says little about bulk publish from markdown files.

Vendor integration pages

Tools like SEOmatic advertise Sanity integrations with Portable Text output and drip publishing. Marketing pages describe scale. They do not show QA hard gates, custom block types, or a Draft-first editorial contract. You learn that programmatic SEO exists. You do not learn how to wire your own converter.

The markdown escape hatch

A popular DEV post recommends `sanity-plugin-markdown` to avoid Portable Text entirely. Fair for teams whose front-end renders markdown strings. Wrong trade for schemas that already define `table`, `youtubeVideo`, or other custom Portable Text types. You lose structured blocks when you store raw markdown.

What the SERP misses: a named framework connecting markdown ingress, custom Portable Text emission, image asset upload, and pre-publish QA before `createOrReplace`.

The MDP Gate: Metaflow's three-stage publish stack

The MDP Gate is Metaflow's original three-layer stack for sanity programmatic publishing. Each layer maps to a script in `apps/web/scripts/`.

M-layer: Markdown authoring

Input: hand-authored or chat-authored `draft.md` plus structured `brief.json` (persona, PAA, intent gaps, IG score). No LLM inside the publish script. Cursor drafts in chat; the script handles mechanics. This matches the Claude skills for blog content writing pattern: separate authoring from publish.

P-layer: Portable Text conversion

`markdownToPortableText()` in `blog-md-to-portable-text.mjs` reads the draft line by line. It builds paragraph blocks. It finds markdown tables. It maps headings to h2, h3, and h4 styles. It parses links and bold text. It turns `::youtube` lines into `youtubeVideo` blocks. The result is a block array ready for Sanity.

D-layer: Draft publish with QA

`publish-from-files.mjs` orchestrates enrich, QA, optional hero, then `publishBlogPost`. Hard gates block `--apply` unless brief IG is at least 7, tables are at least 2, PAA is answered, persona and JTBD align, and link minimums are met. Pass `--force` only when you want a broken Draft for manual Studio repair.

MDP layerCanonical scriptInputOutput
M: MarkdownChat + draft.mdBrief + prosePublish-ready markdown
P: Portable Textblog-md-to-portable-text.mjsMarkdown stringbody block array
D: Draftpublish-from-files.mjsBrief + enriched MDSanity Draft document
Rich text comments on your Next.js blog with Portable Text | Simeon Griggs

Portable Text conversion: what blog-md-to-portable-text.mjs actually emits

The converter is narrow by design. It covers what Metaflow blog posts use today. Sanity programmatic publishing does not need every Portable Text feature on day one.

Block styles and inline marks

Headings become blocks with `style: 'h2'`, `'h3'`, or `'h4'`. Paragraphs use `style: 'normal'`. Inline parsing handles `anchor` as `link` markDefs, `bold` as `strong`, and `italic` as `em`. Each span gets a random `_key`. Portable Text requires stable keys for Studio patches.

Custom blocks: table and youtubeVideo

Tables trigger when a pipe-delimited row is followed by a separator row matching `|----|`. Rows map to `_type: 'table'` with `cells` arrays. The first row sets `isHeader: true`. YouTube embeds use the `::youtube` directive, not raw iframes. The front-end `@portabletext/react` component map renders `youtubeVideo` consistently with AEO-oriented post structure.

What the converter deliberately skips

Placeholder FAQ stubs are stripped. HTML tables are unsupported; use markdown pipe syntax. Code fences are not emitted yet. Add a `code` block handler when your schema requires it. For HTML ingress, Sanity documents `@portabletext/block-tools` `htmlToBlocks` as the inverse path. MDP chooses markdown-as-source because editorial drafts already live in `.md` files.

Image upload and hero assets in the publish path

Hero images are cover assets, not inline Portable Text images. Metaflow separates them so chat-authored posts get a consistent 16:9 spectral graphite hero without editors uploading in Studio.

generateHeroImage and OpenRouter

With `--with-images` and `OPENROUTER_API_KEY` set, `publish-from-files.mjs` calls `generateHeroImage(title)` from `blog-images.mjs`. Gemini returns a URL. The script saves a `hero-image.json` artifact for audit.

uploadImageFromUrl to Sanity assets

`uploadImageFromUrl` fetches the image buffer and calls `client.assets.upload('image', buffer, { filename })`. Sanity returns `asset._id` as a reference target, not a hotlinked URL.

coverImageRef on the post document

`coverImageRef(assetId, alt)` builds `{ _type: 'image', asset: { _ref }, alt }` on the post. `publishBlogPost` also mirrors it into `seo.ogImage`. Inline body images would need a separate `image` block type in the converter. Hero upload covers the batch-blog case where one cover suffices.

The Draft flow: QA hard gates before createOrReplace

`publish-from-files.mjs` is four stages. Understanding them prevents surprise QA failures when you run sanity programmatic publishing at volume.

Dry-run vs --apply

Default invocation is dry-run. Enrich writes `enriched.md`. QA prints metrics. No Sanity write happens. Add `--apply` to call `createBlogSanityClient()` and upsert. Add `--with-images` for hero generation. Missing `SANITY_API_TOKEN` fails at apply by design.

Enrich: internal links and YouTube injection

Enrich runs `injectInternalLinks` from `blog-internal-links.mjs` and `injectYouTubeEmbed` from `brief.asset_plan.youtube`. It appends FAQ sections only if the draft lacks them. Real answers must exist in the draft. Placeholders fail QA.

When --force is acceptable

`--force` publishes despite QA failure so editors can inspect a broken Draft in Studio. Use sparingly. The content engineering framework treats force-publish as a systems failure signal, not a normal ship path.

QA checks include word count, H2 count vs outline, tables, bullets, humanizer cadence, opening stat, internal links, external links, meta lengths, primary keyword density, and full PAA coverage.

MDP vs alternatives: which Sanity publish path to pick

Not every team needs the MDP Gate. Pick based on who authors, what your schema requires, and whether editorial QA is a hard rule. Sanity programmatic publishing still works without MDP if you accept manual Studio work.

Studio manual entry

Best when volume is low and writers already work in Studio. Portable Text fidelity is perfect because the editor produces blocks natively. Does not scale when drafts originate outside Sanity.

Markdown plugin storage

`sanity-plugin-markdown` stores markdown strings. Front-end renders with `react-markdown`. Fast to adopt. You sacrifice custom Portable Text types unless you bolt on separate fields.

Vendor bulk integrations

SEOmatic-class tools connect via Content Lake API and claim Portable Text output. Good for teams buying scale. You inherit their QA model and their template logic, not yours.

Publish pathPortable Text fidelityEditorial QA gateCustom blocksBest for
Studio pasteHighManualFullLow volume, Studio-native authors
Markdown pluginLow (string storage)ManualLimitedMarkdown-first front-ends
Vendor bulk (SEOmatic)Medium (vendor-defined)Vendor-definedVendor-definedOutsourced programmatic SEO
MDP Gate (scripted)High (custom converter)Automated hard gatestable, youtubeVideoContent engineering teams

For B2B SaaS brands publishing ten or more posts monthly, sanity programmatic publishing through the MDP Gate preserves Portable Text structure. It enforces the same gates as every other Metaflow batch post. It lands Drafts editors can approve without reformatting.

Install path: place `brief.json` and `draft.md` under `apps/web/scripts/blog-publish-plan/briefs/`, dry-run QA, fix issues, then run `publish-from-files.mjs` with `--slug`, `--apply`, and `--with-images`. Review the Draft in Studio. Set status to Done and `publishedAt` when ready to ship. Explore more pipelines in the Metaflow learning center.

Frequently Asked Questions

How do you publish to Sanity programmatically?

Authenticate with a write-enabled API token. Instantiate `@sanity/client`. Build a document object matching your schema. Call `create`, `createOrReplace`, or `patch`. Metaflow's `publishBlogPost` uses `createOrReplace` on `post-{slug}` so re-runs update the same Draft. The body field must be a Portable Text array unless your schema uses a markdown type. Sanity programmatic publishing through MDP automates that assembly.

What is Portable Text in Sanity?

Portable Text is JSON for rich text. It is an ordered list of blocks and custom objects. Each text block has child spans and link marks. It also has a style: normal, h2, or h3. The Portable Text specification works with any CMS. Sanity stores it in the Content Lake. GROQ returns it as written. Your site renders it with `@portabletext/react`.

Can you use Markdown with Sanity CMS?

Yes, three ways. Store markdown strings via `sanity-plugin-markdown` and render client-side. Paste markdown into Studio's block editor and let Studio convert to Portable Text. Or keep markdown as pipeline source and convert before API write, as MDP does. The third path preserves custom block types your schema already defines and is the core of sanity programmatic publishing.

How do you upload images to Sanity via API?

Use `client.assets.upload('image', buffer, { filename })` with your auth client. You get back an asset `_id`. Put that ref on image fields. Metaflow pulls hero URLs into a buffer first. See `uploadImageFromUrl` in `blog-images.mjs`. Sanity's Assets API lists rate limits and file types.

What is the Sanity Content Lake API?

The Content Lake is Sanity's hosted document store exposed via HTTP APIs and the JavaScript client. GROQ queries read documents. Mutations write them. Real-time listeners propagate changes to Studio and connected front-ends. Sanity programmatic publishing is authenticated mutations against that store. Same surface Studio uses, without the UI.

How do you create draft documents in Sanity?

Set a `status` field to `Draft` on create and leave `publishedAt` null. Your Next.js front-end should filter unpublished posts. Metaflow sets `status: 'Draft'` in `publishBlogPost` and logs a reminder to flip status in Studio. MDP uses an explicit status field for clarity in batch pipelines.

Related reads

  • 6 Best Programmatic SEO Tools for FramerSep 2025
  • How to Automate Blog Posts in Framer CMSSep 2025
SEO Automation
  • AI Content Pipeline Software for Modern Inbound EngineeringJan 2026