Contentful to git-based blog migration: keep your rankings
A Contentful to git-based blog migration playbook: convert Rich Text JSON and entry references to Markdown, protect your rankings, and cut the metered API bill.
A Contentful to git-based blog migration playbook: convert Rich Text JSON and entry references to Markdown, protect your rankings, and cut the metered API bill.

Contentful is the odd one out in this migration series. WordPress, Webflow, and Ghost all host your site; Contentful doesn't, and never has, so there's no domain to point elsewhere and no platform-side redirect tool to lean on or work around. Notion and HubSpot lock you into a fixed URL pattern you have to map before you can even plan a redirect; Contentful, as a true headless CMS, has no URL pattern of its own at all. Your frontend already owns that.
That changes where the risk actually sits. It isn't a broken slug scheme or a hosting cutover. It's a Rich Text field that comes back from the API as a JSON tree instead of HTML or Markdown, and a web of entry references, authors, categories, tags, related posts, that only resolve through a second object buried in the same API response. Get the conversion wrong and you don't lose a URL, you lose the formatting and the relationships inside every post you migrate.
This post covers why teams leave Contentful in the first place, how to convert its Rich Text and reference graph into clean Markdown without losing structure, the redirect checklist that protects your rankings even though Contentful itself has no redirect tool, and what changes once your posts are files in a repo instead of entries in a metered API. It's the same discipline as our HubSpot to git-based blog migration, Notion to git-based blog migration, Webflow to git-based blog migration, Ghost to git-based blog migration, and Framer to git-based blog migration posts, adapted to the one platform in the cluster with no hosting layer to plan around.
A small blog doesn't need an API-metered pricing model, and it doesn't need the governance features Contentful builds for a 4,800-brand enterprise customer base. It ends up paying for both anyway. Contentful's own pricing page puts the tiers plainly: Free at $0 with 10 users, 100K API calls a month, and 50GB of CDN bandwidth; Lite at $300 a month with 1M API calls and 100GB bandwidth; Enterprise at custom pricing with unlimited calls and spaces. There's no tier between Free and Lite, and no usage-based middle ground for a blog that just needs more than 25 content models but nowhere near a million monthly API calls.
Contentful's Free plan used to allow unlimited content models. As of April 30, 2025, that changed to a hard cap of 25, per an analysis of the update, and the cap applies with no grandfathering: existing free-tier accounts over the limit get notified before Contentful suspends them, not exempted. A content model, in Contentful's terms, is roughly a content type: blog post, author, category, FAQ, testimonial. A working blog with even modest structure, posts, authors, tags, a couple of landing-page types, can bump into 25 faster than it sounds.
The jump from there has no middle step. FocusReactive's breakdown of Contentful pricing is direct about the shape of it: a team outgrowing Free faces "an immediate jump of $3,600/year minimum with no graduated option," since Lite starts at $300 a month billed annually. There's no $50 or $100 tier to grow into first. You're either under the Free plan's limits or paying for Lite's 1M API calls and enterprise-adjacent feature set, comments, task management, scheduled publishing, live collaboration, whether your blog needs any of it or not.
Teams that do outgrow Lite land in custom Enterprise pricing, and the trend line there isn't reassuring either. FocusReactive's procurement-data analysis puts average enterprise contracts at roughly $179,000 a year, up 30% year-over-year between 2024 and 2025, "according to SpendHound's contract dataset, the sharpest increase in the CMS category," with total cost of ownership typically running 40 to 60% above list price once API overages, extra environments, and implementation are counted.
On top of that cost trajectory, Contentful's ownership is changing. Salesforce announced a definitive agreement to acquire Contentful on June 1, 2026, expected to close in Q3 of Salesforce's fiscal year 2027 pending regulatory approval, per CMSWire's coverage. Contentful serves more than 4,800 brands, roughly 30% of the Fortune 500, including IKEA, Vodafone, Electronic Arts, and DoorDash. Salesforce's own framing of the deal is about feeding its Agentforce product, not about serving a self-service blog: Jujhar Singh, President of C360 Applications & Industries at Salesforce, said "with Contentful, we complete that picture by adding a native, headless, composable content layer that lets Agentforce dynamically assemble and deliver personalized experiences across every channel, at the speed and scale the AI era demands." A platform being folded into a much larger enterprise sales motion is a reasonable moment to ask whether your blog's roadmap still matches the vendor's.
Even before pricing or ownership changes, the fit was already off for a plain blog. Contentful's Content Delivery API enforces a default rate limit of 55 requests per second, per Contentful's own developer docs; go over it and you get a 429 with an X-Contentful-RateLimit-Reset header telling you how long to back off, usually one second. Requests that hit Contentful's CDN cache are exempt, so a well-cached frontend rarely feels this in practice. But it's still a runtime constraint a static file on disk never has: a Markdown file doesn't rate-limit, doesn't meter bandwidth against a plan tier, and doesn't need a CDN cache layer sitting between your content and a request-per-second ceiling just to stay fast.
None of this makes Contentful a bad product. WebTechSurvey tracks 14,454 live sites running it, 18.72% of the Headless CMS category and the #3 platform tracked. It's a real, well-adopted system built for exactly the scale Salesforce just paid to acquire: multi-brand enterprises coordinating content across a website, an app, and a personalization engine. A blog with one author and a publish cadence measured in posts per week is a different problem, and metered API calls, per-space overages, and a 25-model cap are the wrong constraints to design a blog around.
The mechanical risk in a Contentful migration isn't URLs, it's data structure. Two things need careful handling: the Rich Text field's JSON tree, and every entry reference that field or your content model points at.
Start with Contentful's own export tool, which dumps a space into one JSON file covering contentTypes, entries, assets, locales, tags, webhooks, and roles. It runs against the Content Management API and works on a Free-tier space, so pulling the export itself doesn't force a plan upgrade.
It has real gaps, though. The tool doesn't export space memberships at all. Webhooks come out with credentials stripped, meaning any webhook secret has to be re-added by hand in wherever those webhooks live next, if you keep them. Custom UI extensions built for the space don't export either and need to be reinstalled manually. Treat the export as raw material for conversion, the same caution that applies to every platform's export tool in this series, not a database dump you can restore from if the migration goes sideways.
This is the part of a Contentful migration with no equivalent on a platform like Webflow or HubSpot, where at least the export is HTML or a plain-text blob you can regex your way through. Contentful's own Rich Text documentation is specific: the API returns Rich Text as "a JSON array of nodes that follows the format of an abstract syntax tree." Every node carries a nodeType, a data object, and optional content, value, and marks properties, with a single root node of nodeType: 'document'. Text nodes support six marks: bold, italic, underline, code, superscript, and subscript.
That means converting a post to Markdown isn't a format transform, string in, string out, it's a tree walk. You recurse through the node structure, translate each nodeType (paragraph, heading, list, blockquote) into its Markdown equivalent, and apply each mark as inline syntax. Skip a node type your walker doesn't handle and it silently drops from the output, formatting that looked fine in Contentful's editor just isn't there in the converted post.
Run an actual export and a one-sentence paragraph with bold text comes back looking like this:
{
"nodeType": "paragraph",
"content": [
{ "nodeType": "text", "value": "Ship the ", "marks": [] },
{ "nodeType": "text", "value": "redirect map", "marks": [{ "type": "bold" }] },
{ "nodeType": "text", "value": " before you touch the export.", "marks": [] }
]
}A walker has to concatenate the value strings in order and wrap the one carrying a bold mark in **, which is the only way you land on Ship the **redirect map** before you touch the export. A regex against the raw JSON won't do this correctly once marks start overlapping or a node nests a list inside a blockquote, which is why a tree walk, not a find-and-replace pass, is the only reliable approach.
Embedded content makes the walk harder, not easier. An embedded-entry-block, embedded-entry-inline, embedded-asset-block, entry-hyperlink, or asset-hyperlink node carries no content of its own, only a sys object with an id and a linkType of Entry or Asset. The actual embedded entry or asset data lives in a separate includes object at the top level of the API response, under includes.Entry and includes.Asset. Contentful's client SDKs resolve this automatically at runtime, so a developer who's only ever queried the space through the SDK may not have noticed the includes object exists. A migration script working straight off the raw export has to build that same lookup by hand: for every embedded reference node, find the matching record by id in includes, pull its actual fields, and splice the rendered result into the Markdown at that point in the tree, an image, a code snippet, a pull-quote from another entry.
The same includes-object pattern applies outside Rich Text too. Any reference field on a blog post entry, author, category, related posts, tags modeled as linked entries rather than a simple string list, comes back as a sys.id pointing at another entry. Outside your Contentful space, that id is meaningless. A frontmatter field that just copies the raw id ships broken metadata: an author byline that reads as a UUID instead of a name.
The fix is the same resolution step as the Rich Text embeds: pull every referenced entry's id, cross-reference it against the full entry list your export or a paginated getEntries call returned, and write the resolved value, the author's actual name, the category's actual label, into your new post's frontmatter rather than the id. Do this once as a lookup table keyed by id before you convert any post, so every reference across your whole export resolves from the same table instead of re-fetching per post.
Here's the reassuring part specific to Contentful: since it never hosted your site, the redirect and DNS mechanics of this migration are simpler than on a platform like Webflow or HubSpot, where leaving also means leaving the host. What has to survive is your content, not a domain cutover.
Contentful is a true headless, API-first CMS, per FocusReactive's overview: "no built-in frontend layer (the 'head')," with teams commonly rendering it through Next.js or another framework they host separately. That means there's no Contentful redirect tool to lose access to on cutover, unlike Webflow's Site Settings redirects or HubSpot's domain-hosted redirect rules, both of which stop working the moment you leave that platform's hosting. Your redirect layer already lives wherever your frontend is deployed, and it stays there. The migration doesn't touch it structurally, it just changes which URLs the redirect rules need to point at.
Pull every URL Google has actually indexed from Search Console's Coverage report, not just the entries listed in Contentful's space. A post unpublished eight months ago, or a category page an old content model no longer generates, can still be indexed and still sending traffic through a stale backlink. If it disappears with no redirect when the frontend swaps to reading from Markdown instead of the Content Delivery API, that traffic and the link equity behind it disappear with it. Build a spreadsheet, old URL to new URL, before you convert a single entry. Since your frontend already decides the URL pattern today, most of these URLs will map one-to-one to the same slug on the new content source, which is the one piece of this migration genuinely easier than the CMS-hosted platforms in this series.
For any URL that does change, the redirect type carries the ranking signal. Google's documentation on redirect types states that for a 301 or 308, "the indexing pipeline uses the redirect as a signal that the redirect target should be canonical." For a 302, 303, or 307, it explicitly doesn't, "the target page might still be indexed if other canonicalization signals are present," meaning the old URL can keep competing with the new one instead of handing off cleanly. A Contentful-to-git move is permanent, so anything short of a 301 sends the wrong signal.
Google's site-move guidance recommends keeping redirects live "for as long as possible, generally at least 1 year," the window Google needs to transfer signals from backlinks still pointing at old URLs. It also flags a temporary crawl-rate increase right after a move: "after a migration, Google will crawl your new site more heavily than usual," as redirected requests from the old URLs stack on top of normal crawl demand. Make sure whatever's serving your new Markdown-backed build can handle that spike before you flip the switch.
The tradeoff is real and worth naming honestly rather than framing the move as a strict upgrade.
Contentful's Studio is built for exactly what a distributed content team needs: several editors working the same space, structured content models that stop a writer from breaking the schema, comments and task assignment on Lite and above. A git-based blog trades that for every post becoming a Markdown file on a branch, reviewed as a diff. For a content team of any size that's never touched git, that's a real cost, not a footnote, and it's the honest reason a team with genuine multi-editor, multi-channel needs might stay on Contentful even after outgrowing its Free tier. If your team is weighing that tradeoff for a specific setup, talk to the founder rather than guessing from a blog post.
What you get in exchange is a publishing pipeline that behaves like the rest of your product instead of a separate API-metered service with its own billing surface. Nothing publishes without a merged pull request, a meaningfully different failure mode than an entry going live in Contentful the moment someone clicks publish in Studio. Sanity vs git-based blog covers the same gap from a different angle: Sanity versions its schema as code but not its content, so a "content as code" vendor pitch and an actual PR-reviewed publish flow turn out to be two different claims, one that applies to Contentful's contentful-migration scripts for content models just as much as it does to Sanity.
Unlike the fully-hosted platforms in this series, a Contentful setup never generated your sitemap, your meta tags, or your JSON-LD schema for you either, your frontend always had to do that work. Migrating to git doesn't hand you something new here, it just moves where that same frontend logic reads content from. What genuinely improves is enforcement: you can gate every merge on checks a headless setup running through a webhook-triggered rebuild has no equivalent for. Our GitHub Actions SEO checks post covers four automated jobs, broken links, valid canonicals, valid JSON-LD, Core Web Vitals budgets, that fail a build before a bad page reaches production, a stronger guarantee than trusting a webhook fired and a rebuild succeeded. And our schema markup for AI Overviews post covers which structured data types are worth adding at build time versus which are vendor invention, useful either way but especially once you're writing that logic once in your own repo instead of maintaining it against an API surface Contentful can change.
If you're checking whether an agent or integration has direct write access to your Contentful space today, our MCP servers for blogs post covers what Contentful's own remote MCP server exposes, full CRUD on entries, assets, and content types through the same Management API contentful-export uses, and where read-only or PR-gated access is the safer middle ground during a migration window specifically.
Work through this before you run contentful-export:
contentful-export against your space, and separately note any webhooks with credentials and any custom UI extensions, since neither exports cleanly.nodeType to Markdown syntax and resolves embedded entries and assets through the includes object, not just the top-level fields.sys.id values.Once your entries are Markdown files in a repo, the publishing question changes from "who has Studio access and what role are they assigned" to "what does our git workflow look like for content." A new post is a branch and a file, not an API call metered against a monthly cap. Review happens as PR comments instead of Contentful's comment and task-assignment features. A merge to main triggers the same build and deploy pipeline your product already uses, no separate space, no separate billing relationship to track against usage.
That handoff is also where a repo-native writing tool fits, instead of an integration calling Contentful's Management API into a space you no longer pay for. A git-based AI blog writer reads your repo's conventions, drafts a post as a Markdown file on a branch, and opens a pull request the same way a human contributor would, so nothing auto-publishes and your existing review process still gates every post. It also trades a metered API bill for a flat plan cost you can check in full on Lyra's pricing page before you commit to anything. If your migration is still ahead of you and you're rebuilding your keyword strategy rather than porting an existing Contentful space wholesale, our guide to SEO for SaaS covers picking winnable keywords and building topic clusters before you're locked into any platform's constraints again.
Once your Contentful migration lands on git, Lyra can take over the writing from there, reading your repo's conventions and opening a pull request for every new post, the same review gate you already use for code.
FAQ
Not if you map every indexed URL and redirect it with a permanent 301 before cutover. Contentful has no hosting and no redirect tool of its own, so that layer already lives on your existing frontend host, which means the migration risk sits almost entirely in the content conversion, not the URLs. Google's own site-move guidance treats a temporary ranking dip as expected and says most sites recover within a normal recrawl window once redirects are live.
Because Contentful's Rich Text API returns a JSON abstract syntax tree, not HTML or Markdown. Text nodes carry marks like bold, italic, and code, and embedded entries or assets show up only as a linkType plus an id reference. The actual embedded content lives in a separate includes object in the response and has to be resolved and walked recursively to reconstruct the post as Markdown.
One JSON file covering contentTypes, entries, assets, locales, tags, webhooks, and roles. It does not export space memberships, and any webhook credentials come out stripped, so they have to be re-added by hand. Custom UI extensions built for the space also don't export and need to be reinstalled manually in wherever you're keeping the content model going forward, if anywhere.
No, contentful-export works against a Free-tier space through the Content Management API, so the export itself doesn't force an upgrade. The pricing pressure that pushes teams to migrate is ongoing, not a one-time export cost: Free caps content models at 25 with no grandfathering as of April 30, 2025, and the next tier up, Lite, is $300 a month with no step in between.
They don't, not automatically. A reference field on a Contentful entry is a sys.id link to another entry, meaningless outside your space until it's resolved. You have to walk the includes.Entry array the API returns alongside every entries.getMany call, match each id to its actual record, and write the resolved name, not the id, into your new post's frontmatter.
Built by the tool you're reading about
Lyra finds the topics worth ranking for, writes them in your repo's voice, fact-checks every claim, and opens a pull request scored and ready to merge. You review and hit merge. Want to see what she'd write for you? Start free with three posts, no card.
Keep reading

A Substack to git-based blog migration playbook: keep your subscriber list, redirect the /p/ URLs, and protect the rankings you already earned.

Content pruning for AI-era SEO means deleting or merging thin posts, not refreshing them. A decision framework, an audit, and what Google's 2026 update changed.

Integration page SEO: why 'connect X to Y' pages convert high-intent buyers, and the template that scales from 50 to 500 pages without thin-content penalties.