Drupal case study ยท 03

Multi-Source Content Migration

A staged migration that consolidated years of content from HubSpot and separate WordPress sites into one Drupal site while one source was still actively publishing.

Problem
Years of articles were spread across HubSpot and separate WordPress sites, each with its own domain, archive, and publishing tools.
Built
A repeatable Drupal migration for both sources: exported WordPress content for a planned cutover and batched API imports for the live HubSpot content, with custom handling for articles, authors, images, categories, and publication state.
Result
The blogs and their archives were consolidated under one domain and one Drupal site, giving readers one place to find the content and editors one publishing workflow to manage it.
  • Migrate API
  • Entity API
  • Plugin API
  • HTTP Client
  • Views
  • Taxonomy
  • File API
  • Content Moderation

Why we built it

A nonprofit publisher had built up years of articles across separate publishing systems and domains. Some blogs lived on HubSpot while others lived on WordPress sites, each with its own address, editorial tools, and archive.

Much of the material was evergreen. Articles published years earlier were still drawing readers, so the migration needed to preserve the full archive rather than only recent content. The goal was to bring those blogs together on one Drupal site and one domain without interrupting publishing while the new site was being built.

My role

This was a team project, and the migration was mine. I designed and built the imports for both platforms, including the migration configuration and custom processing that transformed incoming content, resolved authors, handled publication state, and moved files into Drupal.

For the live source, I also built the authenticated API handling needed to retrieve content in batches using paging and date ranges. I contributed to the day-of-year ordering and calendar archive used to present the migrated content. Other developers built the rest of the site around it.

What it unlocked

Readers could find the organization's blogs and archives under one domain instead of moving among separate sites.

Editors gained one Drupal workflow for drafts, moderation, revisions, and publishing across content that had previously lived in different systems. Older articles also became part of the site's active content experience instead of remaining separated across legacy archives.

What I built for the migration

Articles, author accounts, author profiles, and categories became normal Drupal content as the migration ran. Author matching required special handling because the same person could appear in both sources, might not exist cleanly in the source user data, or could arrive without a usable email address.

The two sources also required different migration strategies. WordPress could be exported and treated as fixed for the final cutover, while HubSpot was still publishing and had to be read repeatedly through an authenticated API using paging and date ranges. I built the custom handling around that API and configured the migration so its source endpoint could change by environment, allowing the same migration setup to run in sandbox, staging, and production.

How the migration ran

The work moved through four stages. We first ran the migration in a sandbox and built the site on top of real migrated content. That proved the approach before committing to the final cutover. Staging followed later, with another live-source import picking up content published since the earlier run. When the new site was ready, the customer froze publishing and we performed the final migration and domain switch.

Because the live-source migration ran more than once, every run needed to behave predictably. Authors were matched using stable source identifiers, previously fetched images were reused, and content with an unresolved author received a defined fallback instead of stopping the import.

After each run, I reconciled the results against the source: exported rows were compared with articles created, and the live platform's record total was compared with what had landed in Drupal.

Drupal under the hood

Migrate API and process plugins
Moved articles, authors, and profiles into Drupal while transforming dates, publication state, author identity, and other source data.
HTTP client and configuration
Handled authentication, paging, and date-range requests needed to migrate content from the live API.
Entity, Field, and Taxonomy APIs
Turned imported articles, authors, profiles, and categories into normal Drupal content that editors could manage after launch.
File API and Content Moderation
Reused previously downloaded images across repeated runs and placed imported articles into the appropriate publishing state.

Why I chose this approach

I designed the migration as a process we could rehearse and repeat, not as a one-time import at the end of the project.

The organization was still publishing while the new Drupal site was being built, so waiting until launch day to move everything would have created too much risk. We first imported the content into a sandbox and built the site around real migrated data. Later runs refreshed the content in staging, and only when the new site was ready did the customer freeze publishing for the final migration and cutover.

That also meant the imports had to behave predictably every time they ran. Authors were matched using stable source identifiers, files that had already been downloaded were reused, and incomplete source data had defined fallback behavior instead of stopping the migration.

The two source systems fit into that process differently. WordPress could be exported and treated as fixed for cutover, while HubSpot was still live and needed to be read repeatedly through its API. Handling each source according to how it actually behaved let us keep publishing during the project while still arriving at one complete Drupal site at launch.

After each run, I compared what landed in Drupal with the source totals so we could catch gaps before moving to the next stage.