← All case studies Contentful → Strapi

hochzeitsplaza.de left Contentful for Strapi and built its own importer

A guest guide on Strapi's blog says a price rise made the move unavoidable for hochzeitsplaza.de. About 2,000 entries and over 11 GB of assets took about 7 hours. The tool is the author's, and the page prints no euro figure.

Tim Adler published this on Strapi’s blog on 5 August 2025. The page says it was updated on 14 June 2026. The URL spells the product “contenful”. He is describing his own project, hochzeitsplaza.de, and the Ruby tool he wrote for it, strapi_lift. Treat the hours and the byte counts as his.

Contentful had been the CMS for years. He says the pricing never really targeted small or medium projects. A noticeable price increase for this site made a move unavoidable. He does not print the old price or the new one. GraphQL on Strapi was a reason they picked it. The other reasons he gives are leaving a vendor and dropping the bill.

The Contentful → Strapi playbook is the export, the types, and the id you keep so a second run updates rows instead of cloning them. His guide is that path, with the parts that broke on a real export.

The export is a JSON file plus a folder of assets

He starts with the Contentful CLI, content only, assets downloaded:

npx contentful-cli space export --space-id <space-id> --content-only --download-assets

The page uses a placeholder for the space id. What you get is a JSON file named like contentful-export-space-id master-2025-XX-XXTXX-XX-XX.json, plus folders such as assets.ctfassets.net. Later the tool wants that JSON as contentful_content and the asset tree as assets_folder.

Contentful’s CDN also resizes images on the way out. Strapi does not, unless you put a CDN in front. They picked Cloudinary so resizing stayed dynamic. He says you can skip that and serve files from Strapi, and then you live with the sizes you configured in Strapi.

Types exist before the first row moves

strapi_lift does not create content types. You rebuild every Contentful type in Strapi first, fields and relations included. He tells you to keep field names close. If Contentful was snake_case and Strapi is camelCase, the tool can map that.

Every Strapi type needs a contentful_id field. That is how a later run finds the row it already wrote. Once you are sure you will not import again, he says you can delete those fields.

The tool is a Ruby project you clone. The page asks for Ruby “at least version 2.75”, then bundle install, then two env vars, STRAPI_API_TOKEN and STRAPI_URL. I am quoting the version string as printed.

For each Contentful type you write a representer and an intermediate model. The representer reads the export. His article example nests fields, then pulls title, slug, and content out of the de_de locale key, and reads the Contentful sys id into contentful_id. Relations in that JSON are links, not objects. A separate link class holds the id and resolves it during import. Assets can share one generic AssetLink. Categories, authors, and galleries each need their own.

The intermediate model is where the interesting flags live. rich_text marks a Markdown field that may contain images. The tool uploads those assets and rewrites the links before save. link_object turns one link into a relation. link_asset uploads a file and attaches it. api_path is the Strapi endpoint, like /api/articles. contentful_content_type is the Contentful type id, and his example prints 5duKiNPsR20mgISegMYmwK.

Contentful has no single-entry type. Strapi does, for a homepage or a settings doc. He maps that with single_content_type! on the model. If the export has more than one entry and you mark the type as single, only the last entry is kept. That is easy to miss if a “homepage” type quietly has two drafts.

A Strapi representer then lists the scalar fields. Relations are a later step, so you leave them off that list. EntriesImporter has an explicit array of models. A type missing from the array is skipped, which he treats as a feature for test runs. The reset command in bin/strapi_lift has its own list. A type you forget there will not be cleared when you wipe a bad import.

Seven hours, and a log that says 1,011 articles

The import he prints is:

./bin/strapi_lift import --contentful_content contentful_export.json --assets_folder assets

You can start it again. It looks up contentful_id and updates. A log line from 17 May 2025 shows article 4dukPV8tjWOGGKEoeAuOUm as Processing 1/1011. That 1,011 is the articles counter in the log. Separately, he says the project was about 2,000 entries and over 11 GB of assets, and the whole migration took about 7 hours. I would not add those into one total. Duration, he says, tracks entry count, asset size, and how fast Strapi and the CDN are.

Relations are resolved one level deep, on purpose, so a circular link cannot recurse forever. Every entry still has to be imported at least once or its outgoing links never get set. The order he describes is assets that sit inside rich text, then entries with their values, then assets linked onto entries, then relations once the target rows exist.

He also prints subset flags. --content-types can take something like articles:10,categories. --ids limits the run to specific entry ids. --skip starts partway through. bin/strapi_lift reset deletes the types you listed.

Logs go to the console and to log.jsonl. He filters errors with jq:

cat log.jsonl | jq 'select(.level == "error")'

The failure he warns about is a lying error. The line you see is often an asset or a link. The cause is an earlier entry, a missing file or a bad link. He says to search the log for that contentful_id and read the lines above it.

Contentful exports sometimes contain 0-byte assets when a download failed. fix-assets re-downloads those. Without contentful_id on the Strapi type, the tool cannot match old and new rows. Field names that do not match fail the same way. And if a linked type was never imported, the relation stays empty.

What he kept, and what the tool still will not do

He says the mapping work up front is cheaper than hand fixes later, and that a test subset before the full run is how this stayed recoverable. Aligning the two models also shrinks the representers.

On cost, the only figure is the shape of the market, not a bill. As of mid-2025, he says Contentful was pushing many old plans onto much more expensive ones. Strapi was cheaper for them even after paying for Cloudinary. There is no euro amount next to either sentence.

He also says the tool needs polish. A central YAML file would beat a Ruby class per type. Complex edge cases and heavy localization are not fully automated. Some of the mapping stays custom. That last limit is the one I would read twice if the space is more than one locale, because his representer is written around a single de_de key.