Ezi Home Services migrates from Contentful to Draftbase
A narrow request — "migrate our blogs to Draftbase" — grew into a full CMS cutover across three repos: blog, legal, contractor guides, course lessons, and a mobile lesson player.
Updated
3
Repos migrated
9/9
Lessons backfilled to Draftbase IDs
1/1
Course backfilled to Draftbase IDs
0
User-facing regressions
Scope
| Repo | What moved |
|---|---|
| ezi-business | Blog (list, detail, topics), legal pages, team/author pages, contractor guides, homepage/services blog teasers |
| ezi-backend | Lesson content, lesson quizzes, final exams |
| ezi-mobile | Lesson player — Contentful removed entirely, no fallback |
Key files: src/utils/draftbase.ts, src/utils/renderMdxContent.tsx, src/components/Base/MdxContent.tsx (ezi-business); src/lib/@draftbase/index.ts replacing src/lib/@contentful/index.ts (ezi-backend); src/utils/draftbase.ts replacing the deleted src/utils/contentful.ts (ezi-mobile).
Where Draftbase made things easier
Plain MDX instead of a rich-text AST
Contentful required walking a Document node tree via @contentful/rich-text-react-renderer. Draftbase content is just MDX text — that renderer came out entirely.
Real component resolution via compileMDX
Internal entry links resolve to real hrefs through MDX’s components override map, instead of a regex string-replace.
CDN-cacheable delivery API
Cache-Control headers and an SDK cacheTtlMs option replaced a hand-rolled in-process Map cache that only lived for one build.
A dedicated React Native renderer
createReactNativeRenderer({ Text, View, Image }) replaced what would have been a manual per-tag component map for the mobile lesson player.
Before: walking Contentful's rich-text tree
import { documentToReactComponents } from '@contentful/rich-text-react-renderer';
function renderBody(document: Document) {
return documentToReactComponents(document, {
renderNode: {
[BLOCKS.PARAGRAPH]: (node, children) => <p>{children}</p>,
[INLINES.ENTRY_HYPERLINK]: (node, children) => (
<a href={resolveEntryLink(node.data.target)}>{children}</a>
),
// one case per node type the schema can produce
},
});
}After: compiling plain MDX
import { compileMDX } from '@draftbase/renderer';
function renderBody(source: string) {
const result = compileMDX(source, {
components: { EntryLink },
});
return result.ok ? <result.Content /> : null;
}Migration decisions that kept it seamless
Rendering split by interactivity. Static content (blog, legal, guide bodies) compiles via compileMDX + renderToStaticMarkup inside getStaticProps, serialized to an HTML string — required because Next.js Pages Router's getStaticProps boundary only accepts JSON-serializable props. The one interactive page (contractor guides checklist, which persists checkbox state) compiles MDX client-side instead, trading SEO for that page in exchange for interactivity.
Dual IDs on the backend for backward compatibility. Lesson and Course models kept contentfulEntryId and draftbaseEntryId side by side, so old app builds kept working while new code paths preferred Draftbase when present. Mobile, by contrast, went Draftbase-only with no fallback branch, per explicit instruction.
Backfill by title match. A one-off script matched existing Mongo Lesson/Course docs to their new Draftbase entries by title — dry-run first, then applied to prod (9/9 lessons, 1/1 course matched cleanly).
Preserved external-link behavior. Contentful's old renderNode auto-added target="_blank" to external links; Draftbase's MDX output doesn't, so a regex post-process was added to restore it.
Post-migration pagination cleanup. The blog index was rendering 100+ posts in one grid, causing sluggish scroll. Restructured into statically-generated paginated routes via getStaticPaths, preserving crawlability for SEO.
Outcome
Clean cutover across all three repos with backward-compatible data (dual IDs on backend models) and no user-facing regressions. Net simplification: less rendering code (no rich-text AST walker), real caching instead of a rebuild-scoped in-memory cache, and a purpose-built React Native renderer — while picking up a pagination fix to the blog index as a bonus during the rework.
Planning your own migration
Bring content types, entries, and assets over with the SDK's migration engine, or start from a template and cut over module by module.
Hobby is free, no card. Startup is $49/mo when you outgrow it. The price is on the pricing page, where prices go.
No migration quarter, no kickoff workshop. Define a template and ship something today.