Contentful Migration
This module helps migrate content from Contentful to Drupal by using the core Migrate API. It handles complex content types like rich text, embedded entries and assets, and cross-references, converting them into standard Drupal entities. The module is flexible and adaptable to different Contentful space structures, and it includes tools to assist with the migration process for various presentation modes.
Contentful Migration moves a Contentful space export into Drupal using the core Migrate API, handling the parts a YAML-only migration can't: Rich Text, embedded entries and assets, media, and cross-reference links.
It is a runtime, not a fixed content model. Every Contentful space is modelled differently, so the module executes per-space migration YAML rather than shipping one schema. The worked migrations in migrations/examples/ are templates you adapt to your space. (The same mapping YAML can optionally be generated by companion content-model analysis tooling.)
What it does
- Rich Text AST → HTML with embedded entry/asset resolution and inline hyperlink rewriting. The contentful/rich-text library's default renderers emit Contentful-id placeholders (visible garbage) or silently drop unknown nodes. This module owns the full renderer list, resolves sys.id references to the already-migrated Drupal entities, and emits clean
<drupal-entity-embed>/<drupal-media>tokens — ending in a logging catch-all that never silently empties. Inline entry-hyperlinks become links to the target entity's canonical page (carryingdata-entity-uuidfor Linkit-style alias-safe rewriting); asset-hyperlinks link to the migrated file's URL when the media + file modules are installed (plain-text degrade otherwise — nothing silently lost). The bundledrecipes/contentful_embed/ships the text format that renders the tokens: embedded assets on core media'smedia_embed, embedded-entry tokens via contrib Entity Embed (tag pre-allowed; two-step documented). - Assets → Media, staging files into Media entities and de-duplicating identical bytes by SHA-256 (self-contained — no migrate_file_to_media).
- Reference fields → internal links, rewriting Contentful entry/asset reference fields to alias-safe
entity:link URIs. - Locale flattening and a two-pass migration (entities first, bodies second) so embeds resolve against already-migrated content.
- Multi-value Paragraph references via the idiomatic core #2890844 workaround.
- Authorship timestamps —
sys.createdAt/updatedAtmap tocreated/changedwith two core process plugins (worked example included). - Author attribution (opt-in) —
drush contentful:export --include-usersstages the space’s members entry-shaped inusers.json(exports alone carry only opaque author ids — 204/211 profiled, zero user objects); thecontentful_userexample imports them as blocked stubs (status: 0, no roles, no password — attribution targets, never logins) and entries resolveuidviamigration_lookup. Duplicate and over-length display names are made safe deterministically at the export step, so delta re-imports never rename members. Off by default; a no-fetchstatic_mappath is documented. - Repeatable / delta imports — re-export then re-import updates changed entries in place (
track_changes, kernel-verified). Deletions don't propagate (a snapshot export has no tombstones) — documented honestly; this is not a sync engine. - A
drush contentful:exportcommand wraps the contentful-export CLI (assets included) to stage a space's JSON + asset binaries ready for migration; the management token is read from theCONTENTFUL_MANAGEMENT_TOKENenvironment variable, never the process argv. - Decoupled conversion toolkit —
drush contentful:jsonapi-mapemits a machine-readable Contentful → JSON:API mapping manifest (content type → resource name, field id → JSON:API field name; pipelines it cannot classify are flagged for manual review, never guessed); the worked examples preserve each entry’s Contentful id infield_contentful_idfor durable JSON:API lookup; and a query-by-query CDA → JSON:API conversion guide (modes/examples/decoupled/CONVERSION.md) covers endpoints, filters, pagination, includes, identity, rich text, and assets.
Migrated content lands in standard Drupal entities (nodes, Paragraphs, Media, menus). All three presentation modes are documented with worked reference templates in modes/examples/: decoupled (the JSON:API enable step and the embed-token contract for SPA consumers), recoupled (an editorInterfaces → Drupal widget map — grounded in 208/211 profiled exports — plus four worked display templates), and semi-decoupled (an honest pointer). One drush recipe command makes embeds render; per-space display config is derived from your export, never shipped.
Project status
Early development (1.0.x, rc). The migration runtime — including Rich Text embed and inline-hyperlink resolution — and the contentful:export Drush command are built and covered by unit and kernel tests, including real two-pass Migrate runs that resolve embeds and hyperlinks, stage assets to Media, attach translations, and render a migrated body through the shipped contentful_embed recipe. Both former roadmap items shipped in 1.0.0-beta1 (presentation-mode templates + embed recipe; asset-hyperlinks to the migrated file). Opt-in author → user mapping shipped in 1.0.0-beta2 (--include-users, blocked-stub author attribution). The decoupled conversion toolkit — jsonapi-map mapping manifest, identity-preservation pattern, CDA → JSON:API conversion guide — shipped in 1.0.0-beta3. Inline-embed resolution and reference-safety hardening shipped in 1.0.0-rc1, the first release candidate (inline-embedded entries now resolve like block embeds; stub-free migration_lookup and NULL-safe reference-revision writes). The roadmap is empty by graduation.
This project is not yet covered by Drupal's security advisory policy.
Requirements
- Drupal 11
- PHP 8.3+
- Core Migrate (the only hard dependency)
contentful/rich-text ^4.0(pulled in via Composer)
Paragraphs, Media, and migrate_plus are needed only when your mapping uses them — see the README.
Getting started
composer require 'drupal/contentful_migration:^1.0@beta' drush en contentful_migration
Then stage your space and adapt the example migrations:
CONTENTFUL_MANAGEMENT_TOKEN=cfpat-… drush contentful:export --space-id=YOUR_SPACE # edit migrations/examples/ to match your content model
Not in scope
Each exclusion is a deliberate decision, with the evidence it rests on (211 real space exports profiled):
- Live/bidirectional sync. One-way migration only; repeatable delta imports cover the re-export case — a live two-way bridge is a different product.
- Full edit/revision history. Exports carry only version counters — none of the 211 profiled exports contain revision snapshots (recovering history would need per-entry Management-API calls). Authorship metadata does migrate: timestamps, and opt-in author → user mapping (
--include-users). - Roles/permissions. Present in most exports (154/211), but Contentful's policy rules don't map onto Drupal's permission model — auto-generating roles risks granting more than intended. Model roles deliberately in Drupal.
- Webhooks, UI extensions, SSO. Contentful platform config with no safe Drupal equivalent.
- Theme/design-system generation. Recoupled presentation consumes a provided theme.
- Contentful CDA emulation. Evaluated and gated, not forgotten: the response envelope is reproducible, but the contract (RichText AST, Images API parametric transforms) is not — 48% of 218 profiled spaces carry RichText fields, 83% carry assets. The decision record with explicit reopen gates ships in the module (
spike/cda-emulation/DECISION.md); conversion is the supported path.