Bunny Honey ClubBunny Honey/blog
Subscribe
← back to indexblog / engineering / why-we-moved-our-blog-off-webflow
Engineering

Why we moved our blog off Webflow and what it cost us

After three years on Webflow we shipped a static MDX blog in a long weekend. Here are the Webflow alternatives we considered, the math, and the cost.

A
ArthurFounder, Bunny Honey Club AI
publishedOct 08, 2025
read5 min
Why we moved our blog off Webflow and what it cost us

We ran our blog on Webflow for three years. It paid for itself the first month — a CMS our writers actually used, a theme our designer could shape, and a publishing flow that didn't require pinging engineering. The trade we made on day one

We ran our blog on Webflow for three years. It paid for itself the first month — a CMS our writers actually used, a theme our designer could shape, and a publishing flow that didn't require pinging engineering. The trade we made on day one was the trade most teams make: lock in a chunk of the stack to skip the bikeshed. Then we wrote eighty posts and the trade started to look different. The right Webflow alternative for our team turned out not to be a different CMS at all — it was no CMS, with the editor replaced by Markdown and components. This piece is the math behind that, the migration log, and the line item we underestimated.

The math that flipped

We weren't paying for a CMS anymore. We were paying for a boundary. Every post passed through that boundary twice: once when a writer dropped Markdown into the Webflow editor, once when an engineer opened the panel to nudge spacing the editor refused to ship. The boundary cost us latency on every change, and it cost us ownership of nothing in particular.

$348Webflow / mo
37 minmedian publish
23%posts needing dev help
0tests we could write

The dev-help number was the one that pushed us. A blog where one in four posts needs an engineer is not really a blog — it's a ticket queue with a hero image. The $348 was almost a rounding error against the time tax. What pushed the move past "interesting idea" into "this weekend" was watching one of our designers spend forty minutes trying to convince Webflow's rich-text editor to wrap a quote in a custom component. She gave up and shipped the post without the component. Forty minutes, a worse post, and a bug we couldn't reproduce.

What we wanted instead

A short list, written in the order it mattered to us.

  1. Markdown in, HTML out. No surprise transforms. No editor that quietly rewrites <figure> tags into <div class="rich-text-figure">.
  2. Components, when we want them. A <Stat>, a <Pullquote>, a <Callout> — the things a normal CMS turns into dropdown menus and quietly breaks every six months when the platform's React version moves underneath you.
  3. Real types on the frontmatter. When cover is missing, the build fails. Not the page.
  4. One publish path. git push. No staging environment that disagrees with prod about what bold looks like.

The reason this list is short is that the list of things we didn't want was much longer, and most of it boiled down to "any feature that exists to make a non-technical person feel comfortable, when our team is now technical." Webflow built a beautiful product for a buyer who isn't us anymore.

The migration, in three afternoons

The corpus was small. Eighty posts, mostly short. We wrote a one-shot Node script that pulled the Webflow CMS API, normalized the HTML to clean Markdown via turndown, and dropped MDX files into content/posts/. About sixty came across with zero edits. The other twenty had bespoke embeds — a YouTube wrapper, a stat grid, an image gallery — and we recreated those as MDX components on the way in.

// scripts/import-from-webflow.ts — the spine of the importer.
import { Webflow } from 'webflow-api'
import TurndownService from 'turndown'
import fs from 'node:fs/promises'
 
const wf = new Webflow({ token: process.env.WEBFLOW_API_TOKEN! })
const td = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced' })
 
td.addRule('stat-embed', {
  filter: (node) =>
    node.nodeName === 'DIV' && node.classList.contains('w-stat-block'),
  replacement: (_content, node) => {
    const items = [...(node as HTMLElement).querySelectorAll('.w-stat-item')]
      .map((el) => ({
        value: el.querySelector('.value')?.textContent?.trim() ?? '',
        label: el.querySelector('.label')?.textContent?.trim() ?? '',
      }))
    return `\n<Stat items={${JSON.stringify(items)}} />\n`
  },
})
 
const items = await wf.collections.items.list({ collectionId: COLLECTION })
for (const post of items.items ?? []) {
  const md = td.turndown(post.fieldData.body as string)
  const slug = post.fieldData.slug
  await fs.writeFile(
    `content/posts/${slug}.mdx`,
    `---\n${frontmatter(post)}\n---\n\n${md}\n`,
  )
}

The stack on the receiving side: Next.js 15 App Router, TypeScript, Tailwind, Velite for the content layer. (Next 16 shipped while we were writing this, and we're already planning the move; we'll cover the upgrade in a dedicated post.) The whole thing static-generates in eleven seconds and ships on Vercel.

You realize the part I was fighting in Webflow wasn't the design. It was that nothing was a primitive. Now everything is.

our designer, mid-migration

The redirect map nobody tells you about

The cost we underestimated wasn't engineering. It was the redirect map. Webflow's CMS uses one slug pattern; we wanted ours flatter. We had four years of inbound links pointing at /blog/categories/<category>/<slug> and we wanted /posts/<slug>. That's six hundred and twelve URL pairs, each of which needed a 301 in the Vercel config to keep PageRank from leaking.

// next.config.js — generated, not hand-written.
module.exports = {
  async redirects() {
    const map = await readJson('./redirects.json') // 612 entries
    return map.map((r) => ({
      source: r.from,
      destination: r.to,
      permanent: true,
    }))
  },
}

We generated redirects.json from the same importer script that pulled the posts. If we'd hand-rolled it we'd still be there. Two weeks after launch, search traffic was within four percent of the prior trend line. The only place we measurably gained was OG-image generation: every post now gets a fresh dynamic OG card on /posts/<slug>/opengraph-image, where Webflow's were three years stale.

What we'd do differently

Two things, both small.

We'd have moved sooner. The thing keeping us on the old stack was the imagined effort of leaving — months, surely, when in practice it was a long weekend. The cost of staying was three years of small papercuts, and the imagined-effort tax kept us paying it.

We'd have built the component library first, then the migration. We did it backwards: got the posts across, then realized we needed <Stat> and <Pullquote> and <Callout> and went back to retrofit half the corpus. If you're doing this, build the components, decide the conventions, then migrate.

// Our entire publish pipeline now.
git add content/posts/new-post.mdx
git commit -m "post: new piece"
git push
// Vercel builds. Cache invalidates. Done.

We're not telling anyone they should leave Webflow. For a marketing site with a small content footprint, it's still the right answer. We're telling you what we noticed at our scale, in our season, with our team. The right CMS for the next blog we ship may well be Webflow again — and if it isn't, the second-best alternative is the one we'll build next, the same way.

What changed isn't our opinion of Webflow. It's our opinion of how much our blog actually changes.

— filed underEngineeringToolingMeta
— share
— keep reading

Three more from the log.