What moving a legacy CMS into Drupal involves
Moving content out of a custom or proprietary CMS into Drupal runs through the core Migrate API, with Migrate Plus supplying source plugins for CSV, JSON, XML and SOAP feeds. The effort is driven mainly by two things: how cleanly the legacy data model maps to fields and entities, and how much content must be rebuilt by hand because it cannot be carried across. Before committing, the team running the legacy system should export a representative sample of content types, relationships, attachments and users, then count how many of those have no direct equivalent in Drupal. That count is the best early signal of scope. Drupal itself has no paid edition. It is licensed under GPL-2.0-or-later, so the cost of the project sits in people and infrastructure rather than in a Drupal license. The target content model is designed, not copied. Legacy tables, flags and free-text fields usually need to become content types, entity references, taxonomy terms and typed fields, and some legacy behavior will be better rebuilt as configuration than migrated as data. Decisions about URL structure, redirects, revisions and who owns each content type belong in the design phase, because they are expensive to change once migration mappings exist.
Work is scoped in writing before it starts. A useful written scope names each source system and export method, the content types in and out of scope, the field mapping document, the approach to users and passwords, the handling of embedded media and internal links, and the acceptance checks used to sign off each migration. Ask the delivery partner to state how incremental runs will re-import changed records, how rollback is tested on a copy of the target site, and which items the client must review by hand. Confirm with the vendor of your legacy CMS whether its export formats are stable and documented, because an undocumented export is often the largest unplanned cost in a project like this.
Which legacy CMS features map cleanly to Drupal and which do not
This table compares common legacy CMS features with their Drupal equivalents and flags the ones that rarely move without rework. Use it as a starting point, then confirm each row against your actual export, because the same feature name can be stored very differently from one custom CMS to another. The main takeaway is that structure and content usually map well, while embedded markup, workflow and layout logic need explicit design decisions.
| Legacy CMS feature | Drupal equivalent | Migration notes |
|---|---|---|
| Content types | Content types | Maps cleanly if each legacy type has a stable set of fields. Confirm the field list per type in the export. |
| Typed fields (text, date, number, reference) | Fields on content types | Usually maps cleanly. Check date formats, character encodings and reference fields that point to other records. |
| Categories and tags | Taxonomy vocabularies and terms | Maps cleanly when values are controlled lists. Free-text tags need deduplication before import. |
| Attachments and images | Media or file entities | Maps with planning. Confirm file paths, sizes and whether files are stored in the export or only referenced. |
| Authors and users | Drupal users and content authorship | Maps partially. Passwords usually cannot move, so plan password resets and check how author IDs are exported. |
| URLs and slugs | Path aliases and redirects | Maps with planning. Build a redirect map from old URLs so existing links and search rankings keep working. |
| Revisions and version history | Entity revisions | Partial. Current versions usually migrate well, while full history often needs a deliberate decision about scope. |
| Editorial workflow states (custom) | Content moderation workflows | Does not map cleanly. Custom states and transitions must be designed and rebuilt, then mapped from stored status values. |
| Proprietary shortcodes or embedded markup | Text formats, filters or Paragraphs | Does not map cleanly. Each shortcode needs a converter or a rebuilt component, and it should be inventoried first. |
| Bespoke permissions | Roles and permissions | Does not map cleanly. Rebuild role design from the access model, not from legacy permission flags. |
| Page layouts and templates | Layout Builder, Paragraphs or theme templates | Does not map cleanly. Layouts are usually rebuilt as components rather than copied as stored markup. |
| Content stored as serialized blobs | Structured fields | Does not map cleanly. Serialized data must be decoded and split into fields by a custom source plugin. |
Documented import paths for a custom CMS source
A custom CMS export can reach Drupal through several documented paths, and most projects combine two or three of them. Each card below names what a tool reads, where it stops, and what your team must confirm before choosing it.
Core Migrate API with custom YAML
The core Migrate API lets you describe each migration as YAML, with a source, a process pipeline that maps each field, and a destination entity type. It is the base layer every other option plugs into, so the mapping you write here is what your editors will live with. Confirm which source plugins ship in your Drupal version and how your team will version the migration files. Note that the core API does not read your legacy CMS by itself; it needs a source plugin for the export format you actually have.
Migrate Plus for CSV, JSON and XML
Migrate Plus adds source and data parser plugins that read remote or local files in CSV, JSON and XML, including paged HTTP endpoints. It fits exports and simple APIs your CMS already produces. Its coverage of SOAP is the part to verify: confirm which SOAP request and response handling the version you install supports, and whether authentication, WSDL handling and pagination need custom code. Test against a full-size export, not a sample, because nested XML and JSON structures often break the field mapping late.
Migrate Source CSV for spreadsheet exports
Migrate Source CSV is a dedicated source plugin for comma-separated files, useful when the legacy CMS team delivers spreadsheets rather than an API. It handles one row per entity and maps columns directly to fields. It does not handle multi-value fields stored in a single cell, embedded HTML with line breaks, or relationships expressed as IDs from another file unless you add lookups. Confirm the character encoding and delimiter of each export before you write the mapping.
Migrate Tools Drush commands
Migrate Tools provides the Drush commands that run, roll back and reset migrations, and report what was imported. These commands are how you rehearse the cutover, rerun changed sources and undo a bad load without rebuilding the site. They do not repair a mapping error by themselves, and rollback only removes entities the migration recorded in its map table. Confirm how your team will store the migration map and whether runs happen from a pipeline or by hand.
Custom source plugin for unsupported formats
When the legacy CMS uses a proprietary database schema, a binary export or a SOAP service that the installed plugins cannot read, your team writes a custom source plugin in PHP. This gives full control over reading, paging and retry behavior, and it is where most of the unique effort sits. It also becomes code you must test, document and keep compatible with each Drupal upgrade. Ask who will own the plugin after launch and whether its tests run in your continuous integration pipeline.
How the source format changes the import work
CSV is the most common export from a legacy CMS and the simplest to read. Each row normally becomes one Drupal entity, such as a node, and each column is mapped to a field, a reference or a property. The format has no native nesting, so repeated values such as tags, authors or related items usually sit in one cell separated by a delimiter, and the import has to split and look up each value. Files are read in a single pass, so pagination and rate limits rarely apply, but very large files still affect memory and run time.
- Row to entity: Confirm whether one row is one node, or whether several rows share a key and must be grouped into one entity.
- Nested values: Multi-value cells need an explicit delimiter and a rule for how each part maps to a reference or a term.
- Encodings and delimiters: Check the character set, byte order mark, line endings and whether commas appear inside quoted text.
- Date formats: Spreadsheet exports often mix day-first and month-first dates, so agree one format and parse against it.
- Duplicate keys: Find repeated legacy IDs or slugs before the run, since a duplicate key can overwrite a record or fail a unique constraint.
Before scoping, ask the legacy owner for a sample of the largest export and the rules behind each column, because hidden conventions in the spreadsheet usually decide the mapping effort.
A JSON API returns records as nested objects and often in pages. Migrate Plus source plugins can read these responses, but the team has to map the response structure to the migration, including where the record list sits in the payload and which fields hold the identifiers. Nested objects can be flattened into several fields or read as separate sources, and related records are usually linked by their legacy ID, so the migration must resolve those IDs to Drupal entities that were created earlier in the run. Pagination, authentication and rate limits all affect how long the run takes and whether it can resume cleanly.
- Record structure: Identify the array that holds the records and the fields that carry the legacy primary key.
- Pagination: Confirm the paging style (page number, offset or cursor) and whether totals are reported, so the run can detect when it has read everything.
- Authentication: Check token lifetimes and refresh behavior, since a long import can outlive a token.
- Rate limits: Ask the owner for the request ceiling and whether throttled responses return a status code the migration can retry.
- Cleaning: Watch for null versus empty strings, inconsistent date formats and HTML embedded in text fields.
Before scoping, ask whether the API is read-only in production, whether a test environment with realistic data is available and how deleted records are reported.
XML dumps are often full-database exports that a legacy CMS writes to a single large file or a set of files. The structure is hierarchical, so the migration must choose a node in the tree as the record and use XPath-style selectors to read each field, including attributes and repeated child elements. Because the file is produced in one step, the main risks are file size, memory use during parsing and the need to split the dump into manageable chunks. Relationships between records often sit in separate sections of the file, so the migration must read those sections before it can resolve references.
- Record selection: Pick the element that represents one entity and confirm that it repeats consistently across the file.
- Repeated and nested elements: Decide whether child elements become multi-value fields, paragraphs or separate related entities.
- Attributes and namespaces: Check for attributes that carry IDs or types, and for namespace prefixes that change selector syntax.
- Encodings: Confirm the declared encoding matches the bytes in the file, and look for entity-escaped characters inside text.
- Size and chunking: Test the parser on the full file to measure memory and time before deciding whether to split it.
Before scoping, ask for the schema or a sample of each section, and confirm whether the dump is complete or excludes attachments that must be fetched separately.
SOAP services expose a legacy CMS through a WSDL contract, with typed operations, envelope-based requests and often session or WS-Security headers. Standard Migrate Plus source plugins are built for plain files and REST-style endpoints, so a SOAP source usually needs a custom source plugin or an extension that calls the operations, handles the envelope and maps the typed response to rows. Each operation may return a limited number of records per call, which makes paging, session expiry and fault handling part of the migration design. Confirm the available options with the vendor or community before scoping, because the work depends on which libraries and extensions are current.
- Operations: List the read operations, their input parameters and the size of each response.
- Authentication and sessions: Find out how sessions start, how long they last and how a long run renews them.
- Paging and limits: Confirm whether each operation accepts offsets or date filters, and whether the service throttles calls.
- Faults and retries: Define how the migration handles SOAP faults, timeouts and partial responses so it can resume without duplicates.
- Types and cleaning: Check how the WSDL types map to Drupal fields, and clean dates, codes and encodings before import.
Before scoping, confirm whether an existing Migrate Plus plugin, a community extension or a custom plugin is needed, and ask for test access to the service.
How inventory, pilot, parallel run and cutover fit together
This sequence moves a legacy CMS into Drupal in controlled stages, so each stage proves the mapping, the content and the permissions before the old system is switched off. Each step closes with a check that content owners or your team can sign off.
Inventory content types, fields, users and integrations
DeployOpen works with your developers and content owners to list every content type, field, taxonomy, user role, file store and integration in the legacy CMS, including the ones that exist only in templates or custom code. Your team provides database or export access, a sample of real records and the names of the people who own each content area. The step is done when the inventory is agreed in writing and every item is marked as migrate, rebuild or retire.
Agree the target content model in Drupal
Using the inventory, DeployOpen proposes Drupal content types, fields, taxonomies, media types and roles, and records where legacy data will not fit a Drupal field without transformation. Your content owners review the proposal against how editors actually work, and your security or compliance lead confirms the role model. The output is a signed content model document that lists each legacy field and its target field.
Build mappings for one pilot content type
DeployOpen picks one representative content type, usually one with relationships, files and multiple values, and builds the source plugin, field mappings and any transformation code for it using the Migrate API and Migrate Plus. Your team confirms the source format is stable, whether CSV, JSON, XML or SOAP, and supplies a test extract. The pilot is complete when records load into a development Drupal site with no unmapped fields and the mapping is documented.
Run full and incremental migrations into staging
DeployOpen extends the pilot mappings to all content types, runs a full migration into a staging Drupal site, and then runs incremental migrations that pick up records changed in the legacy system since the last run. Migration IDs, source keys and rollback steps are recorded so any batch can be reversed and rerun. Your team provides a staging environment with enough storage for files and a database snapshot taken before each full run. The step closes when repeated runs produce the same results for unchanged source data.
Validate counts, links, media and permissions with owners
Record counts are compared between the legacy CMS and staging for each content type, and a sample of nodes is checked by content owners in the Drupal interface. Internal links, embedded media, file paths and user permissions are tested with scripted checks and manual review, and each defect is logged against its mapping. The step is complete when content owners sign off each area and every open defect has an owner and a planned fix.
Run both systems in parallel with synchronized content
The legacy CMS stays live for readers and editors for an agreed period while incremental runs keep Drupal current. Your team decides which system is the editing source for each content area during this period and communicates that rule to editors, since two-way editing is a major source of data loss. DeployOpen reviews sync logs, reruns validation after each cycle and tracks defects until the agreed exit criteria are met. The parallel period ends when the open defect list is empty for critical content and sign-off is recorded.
Cut over with a final incremental run and redirects
During a planned change window, the legacy CMS is set to read-only, a final incremental run brings Drupal fully up to date, and the validation checks run again. URL redirects from legacy paths to Drupal aliases are deployed and tested with a sample of old links, and DNS or routing changes are made according to your hosting plan. The step is done when the final counts match, redirects resolve, and content owners confirm the live site.
Decommission the legacy CMS after handover
Once the cutover is stable, the legacy CMS is archived according to your retention policy, with a final database and file export kept in storage you control, and its services, credentials and integrations are removed. Your security team confirms that access has been revoked and that backups of the old system are handled as your records policy requires. The step closes with a handover document covering the mappings, migration scripts, rollback procedure and open items.
What drives the cost and duration of a CMS migration
This table compares the factors that change the effort of moving content from a legacy CMS into Drupal, with what each one means for the work and what to ask for in a quote. Read it before comparing proposals, because duration is set by the written scope rather than by any fixed figure for this kind of project.
| Driver | Effect on effort | What to ask for in a quote |
|---|---|---|
| Content types and fields | Each source type needs its own mapping, transforms and validation. Nested or polymorphic structures add the most work. | A field-by-field mapping list with the target Drupal content type for each source type |
| Quality of the export | Clean CSV, JSON or XML is cheap to read. Broken encodings, inconsistent dates or undocumented SOAP responses add discovery and parsing time. | Who supplies the export, in what format, and how the provider validates it before handover |
| Media volume and file storage | Large file sets take longer to copy, need checksum checks and may require path rewriting in body text. | Total file count and size, storage location, and the approach to embedded media references |
| Users, roles and permissions | Accounts, password handling and role assignments need separate mapping and a decision on authentication after cutover. | Whether passwords migrate, how roles map, and who signs off on access rules |
| Integrations and feeds to rebuild | Each inbound feed, API or export job that the new site must replace adds build, testing and monitoring work. | A list of every feed, its owner, and whether it is rebuilt, retired or kept running during transition |
| Custom shortcodes and embedded markup | Proprietary tags in body text must be parsed and converted, and each pattern adds rules and review. | A count of distinct embedded patterns and the expected conversion approach for each |
| URL redirect requirements | Preserving old URLs requires a complete URL inventory, redirect rules and checks after launch. | The URL inventory source, redirect rules expected, and how redirects are tested before go-live |
| Multilingual content | Each language adds translation linkage, language configuration and separate validation passes. | Languages in scope, translation storage format and whether machine translation is excluded |
| Parallel runs and validation rounds | Each trial import, comparison and fix cycle repeats mapping and checks. Scope should state how many rounds are included. | The number of trial runs, the validation method, and the acceptance criteria for each round |
| Content cleaning by editors | Editorial review of duplicates, outdated pages and broken links often takes longer than the technical import. | Who performs cleaning, what share of content is reviewed, and what the provider handles versus editors |
What usually goes wrong in custom CMS imports and how to prevent it
Most failed legacy CMS imports do not fail in the Migrate code. They fail in the decisions around it: what was exported, what was lost in transit, what the old URLs were, and what changed while the work was underway. The cards below cover six failure points that recur on these projects, each with the check that prevents it.
Incomplete or undocumented export
A custom CMS often stores fields that no one listed in a spec, such as legacy flags, hidden categories or fields populated only by a cron job. If the export omits them, the Drupal site launches without data that editors assumed was there. Build a field-level inventory from the database schema and the templates, not from the admin screens, and record each field's destination in Drupal or its reason for being dropped. Ask the owners of the old system to sign off on that inventory before any mapping is written.
Data loss in embedded markup
Legacy bodies often contain inline HTML, proprietary shortcodes, serialized PHP or JSON blobs, and image tags pointing at internal paths. A source plugin that reads these as plain text will copy them without error, and the damage only shows when a template renders them. Sample a set of records from each content type, compare the rendered source with the imported result, and decide per pattern whether to convert, strip or keep it. Serialized fields need an explicit unserialize step in the process pipeline, with the failure cases logged.
Broken internal links and media paths
Internal links in legacy content usually use the old CMS's ID scheme or relative paths that resolved only on the old host. After import, those links return 404 pages or point to the wrong node. Build a lookup table from legacy IDs to new Drupal entity IDs during the run, and rewrite links in a post-processing step. Then crawl the new site after every run and check internal links, image sources and file downloads, so that a broken reference is caught in staging rather than by a reader.
Lost URLs and search equity
Drupal assigns its own paths unless the migration preserves the old ones through URL aliases, so a redesigned site can lose years of indexed pages. Export every public URL from the legacy server logs, sitemaps and analytics, not only from the database, because pages generated by routes often exist in no table. Map each old URL to a new destination in a redirect map and load it into the redirect module before cutover. Verify the mapping with a crawl that compares old and new status codes.
Users and permissions that do not carry over
Legacy user tables usually store password hashes in a format Drupal cannot verify, and role models rarely map one to one onto Drupal roles and permissions. Migrated accounts may arrive with no usable login, or with editors who hold roles they should not. Decide early whether accounts are migrated, re-created on first sign-in through an identity provider, or reset with a forced password change. Map roles on paper first, then test each role with a sample user in staging before the cutover.
Editors changing content during the parallel run
When the old system stays live while Drupal is built, editors keep publishing in it, and the new site drifts out of date. A single full import at cutover then either overwrites recent edits or misses them. Choose one of two approaches before the build: a content freeze on the legacy system for a defined window, or a delta process that imports records changed since the last run, keyed on a stable legacy ID and a last-modified timestamp. Agree who owns the decision and rehearse the cutover run at least once.
How rollback and incremental runs protect editor work
Rollback only removes what the migration created, and it finds those entities through the migration map. Migrate Tools records a row in the map for every source record it imports, linking the source ID to the Drupal entity ID it produced. A rollback reads that map and deletes the matching entities, so content an editor created by hand after the import is outside its reach. That protection holds only if the map is intact, so the team should avoid truncating map tables or editing migrations in ways that break the link between source and destination. Because a rollback is destructive, run it against a staging copy loaded from a recent production backup before any production run. Time the full import and rollback sequence there, check that node counts, media, redirects and URL aliases return to their pre-migration state, and confirm that no entities outside the migration were touched.
Incremental runs update only the source records that changed, and this is where editor work is most at risk. A migration configured to update existing rows will overwrite Drupal fields with source values on the next run, including fields an editor corrected after the first import. The mapping decides what happens. Fields can be set to skip on update, mapped only on initial creation, or left out of the update path, and the team should decide that per field before the first run. Confirm how each source identifies its records, whether through a stable primary key, an external ID or a file path, because an unstable identifier causes duplicates or wrong overwrites. Also confirm how the source handles deletions: a record removed upstream stays in Drupal unless the team plans an explicit unpublish or delete step, and that step needs its own test.
Questions teams ask before moving a custom CMS to Drupal
How much does it cost to migrate a custom CMS into Drupal?
Cost depends on the scope written into the quote, not on a standard rate. The main components are discovery of the source system, data analysis and field mapping, custom source plugins for the export format, content transformation rules, the Drupal content model that receives the data, test runs, reconciliation reports and cutover. Drupal itself has no license fee because it is GPL-2.0-or-later software, so the spend goes to people and any hosting you buy. Most of the effort is one-time, while hosting, monitoring and later content loads recur. Ask for a quote that lists each source entity, the number of mapping rules, the export formats, the number of test cycles and who signs off each stage. DeployOpen does not publish a price list, so a quote follows from that written scope.
How long does a legacy CMS migration take?
Duration is driven by how messy the source data is and how many content types must be mapped, not by the Drupal side alone. Expect the longest phases to be source discovery, cleaning inconsistent records, and repeated test imports that surface edge cases. Content volume matters, but the number of distinct content types, embedded media, custom fields and cross-references usually matters more. Incremental runs add time for synchronization planning and cutover rehearsal. Ask for a timeline tied to named milestones in the written scope, and confirm which parts depend on your team, such as export access and content owner reviews.
What must our team provide for the migration?
Three things decide how smoothly the project runs: access to the source data, named people who own the content, and a clear sign-off process. For access, you need a stable export or read-only database or API endpoint, sample exports in each format, and documentation of how the source stores relationships, revisions and files. Content owners must confirm which fields are authoritative, which content is obsolete and how mapped values should appear. Sign-off should be written, with an agreed acceptance checklist covering record counts, sample pages and reconciliation reports before cutover. Confirm these responsibilities in the scope document before work starts.
Can our own developers run the migration with the Migrate API?
Yes, a team with PHP and Drupal experience can build a migration with the core Migrate API and Migrate Plus, and many do. The routine work, such as defining a CSV or JSON source, mapping fields and running migrations, is well documented. The hard parts are a custom or SOAP source that needs its own plugin, transforming inconsistent data, handling entity references and revisions in the right order, building repeatable incremental runs, and designing rollback so that a failed import can be reversed without damaging later content. Your developers will also need to keep the migration code working as Drupal versions change, which is a recurring cost. Decide early whether that maintenance belongs to your team or to a partner.
Can Drupal import from SOAP or XML sources?
Drupal can import from XML and SOAP sources, typically through Migrate Plus source plugins for XML and through custom source plugins where the endpoint or message structure is unusual. SOAP is less direct because it requires calling the service, handling authentication and paging, and parsing the response into rows. Confirm during scoping whether your service returns complete data in a single call, supports pagination or change timestamps for incremental runs, and whether its WSDL is stable. Also check how attachments and binary content are delivered, since these often need separate handling.
What happens to our old URLs and search rankings?
Old URLs stop working unless you plan redirects, so the migration must include a complete URL inventory from the source system. Capture each public path, map it to the new Drupal path or alias, and generate permanent redirects that run in the web server or in a redirect module. Also carry over page titles, meta descriptions and canonical tags where the content still applies. Test redirects against a crawl of the old site before cutover, and monitor 404 errors for several weeks afterward. Ask your partner for the redirect map as a deliverable, because search rankings usually depend on it.
Do we need to run the old and new sites in parallel?
A parallel run is not always required, but it is a common way to reduce cutover risk. It means the legacy CMS and Drupal both serve content for a period while the team compares records, publishes changes on one side and reconciles them. The alternative is a single cutover after a final incremental run, which needs a freeze window on the source and a rehearsed rollback. Choose based on how often content changes, how critical the site is, and whether editors can tolerate a freeze. Write the decision and its rollback criteria into the scope before work begins.
How DeployOpen scopes and delivers a CMS migration
DeployOpen plans, builds, documents and supports self-hosted Drupal inside your own infrastructure, so the migration of your legacy CMS content runs in an environment you control. Engagements start with a written scope that fixes deliverables and timelines before any mapping or import code is written.
How we scope
- We inventory the legacy CMS export formats, content types, media files and their volumes before agreeing a plan.
- We document the source-to-Drupal field mapping for each content type, including fields with no clear equivalent.
- We agree in writing which content, redirects and integrations are in scope, and which are excluded.
How we build
- We build Migrate API and Migrate Plus migrations using CSV, JSON, XML or SOAP sources, with repeatable configuration files.
- We test each migration on a copy of the production data, checking record counts, field values and file references.
- We set up incremental runs and rollback procedures, and we verify rollback on a non-production environment.
How we hand over
- You receive the migration code, configuration, mapping documentation and a runbook for running and rolling back imports.
- We document known data gaps, transformations and manual cleanup steps so your editors and reviewers can check results.
- We walk your team through the cutover checklist and the verification evidence collected during testing.
How we support
- Post-cutover support, bug fixes and further migration waves can be arranged as a retainer, if your scope needs them.
- Ongoing support covers Drupal updates, security patches and dependency upgrades for the migrated site.
- Ask for a quote based on the written scope, including who runs the cutover and who owns the source system after launch.

