← Blog

Drupal

Drupal 7 naar Drupal 11: playbook voor 1.400 pagina's

Dinsdag in mei. De Drupal 7-site staat al zestien maanden voorbij end-of-life. 1.400 pagina's, zeven Feeds-importers waar niemand aankomt en een workflow zonder documentatie.

Jacob Molkenboer· Oprichter · A Brand New Company· 11 jun 2026· 11 min
Gesloten leren logboek met messing sleutel op crème kaart, label met touw, groen lint op ivoorpapier.

Dinsdag in mei. Het telefoongesprek duurde vier minuten. Een Utrechtse woningcorporatie, zestien maanden voorbij Drupal 7 end-of-life, had van een eerder bureau een offerte van €180.000 gekregen voor een volledige herbouw. De werkelijke vraag was kleiner: 1.400 pagina's, zeven Feeds-importers die elke nacht huurwoningdata binnentrekken, en een Workbench Moderation-workflow die de oorspronkelijke ontwikkelaar in 2018 had achtergelaten. Breng het vooruit. Verlies niets.

Drupal 7 is sinds 5 januari 2025 formeel end-of-life. Het Security Team geeft geen advisories meer uit. Composer-dependencies verrotten. De site draaide nog op de dag dat we gebeld werden, maar elke contrib-module-download was een 404 in de wacht.

Dit is het playbook dat we draaiden.

Een audit van 90 minuten voordat er code wordt geschreven

De eerste negentig minuten openden we geen editor. De audit liep tegen de live database via een read-only replica.

drush @d7 status
drush @d7 pm:list --status=enabled --no-core --format=table > enabled-modules.txt
drush @d7 sql:query "SELECT type, COUNT(*) FROM node GROUP BY type" > content-counts.txt
drush @d7 sql:query "SELECT name, state FROM workbench_moderation_node_history" > moderation-history.txt
drush @d7 sql:query "SELECT id, fetcher, parser, processor FROM feeds_importer" > feeds-inventory.txt

Vijftien minuten queryen leverde vijf concrete getallen op:

  • 47 contrib-modules actief, waarvan 12 zonder Drupal 8+ port.
  • 1.387 gepubliceerde nodes, 213 concepten, 41 in "needs review".
  • 3 actieve workflow-states, plus 1 verouderde "Archive"-state waar 17 nodes nog naar verwezen.
  • 7 Feeds-importers, 4 draaiend op cron, 3 slapend sinds 2019.
  • 2 custom modules, samen zo'n 1.800 regels PHP.

De verrassing zat in die Archive-state. Het vorige team was er in 2020 vanaf gestapt, maar had de verwijzingen nooit verwijderd. Migreer je blind, dan landen die 17 nodes in Drupal 11 met een state die niet meer bestaat. De migratie faalt halverwege, de transactie rolt terug, en je zit donderdagmiddag uit te zoeken waarom één node-ID steeds in het errorlog opduikt.

Waarschuwing

Voordat je één migration plugin schrijft, query je de moderation-history-tabel op orphan states. Elke verouderde Drupal-site heeft ze. Ze laten de import stilletjes mislukken als je logging op iets minder dan verbose staat.

Workbench Moderation mappen op Content Moderation

Workbench Moderation was een Drupal 7-only module. In Drupal 8 is hij herschreven als Content Moderation in core, met Workflows als state machine.

De mapping gaat niet automatisch. De namen van de states blijven, maar het onderliggende entiteitsmodel is veranderd. Een D7-node had één moderation-state per revisie. Een D11-node heeft een moderation_state-veld dat verwijst naar een Workflow config entity. De migratie moet:

  1. De Workflow-entity één keer aanmaken, voordat enige node-migratie draait.
  2. De states binnen die workflow aanmaken, met dezelfde namen als in D7.
  3. Transities definiëren, want Workbench Moderation bewaarde die in een workbench_moderation_transitions-tabel zonder D11-equivalent.
  4. States per revisie migreren via een custom process plugin.

De workflow-definitie staat in een config-YAML. De vorm die wij gebruikten:

langcode: en
status: true
id: editorial
label: Editorial
type: content_moderation
type_settings:
  states:
    draft:
      label: Draft
      weight: 0
      default_revision: false
      published: false
    needs_review:
      label: 'Needs review'
      weight: 1
      default_revision: false
      published: false
    published:
      label: Published
      weight: 2
      default_revision: true
      published: true
  transitions:
    submit_for_review:
      label: 'Submit for review'
      from: [draft]
      to: needs_review
      weight: 0
    publish:
      label: Publish
      from: [needs_review]
      to: published
      weight: 1

Voor de 17 verweesde Archive-nodes mapten we naar draft en logden we elke node apart. De redacteur bij de woningcorporatie liep het log door, herstelde er 4 naar published en gaf toestemming om de rest te verwijderen.

De zeven Feeds-importers, getrieerd

De Feeds-module in Drupal 7 is niet de Feeds-module in Drupal 11. Zelfde naam, andere code, ander config-schema.

We triëerden de zeven importers op vier vragen. Draait hij nog op cron en levert hij data die de site nodig heeft? Bestaat het bronformaat (CSV, XML, JSON, RSS) nog? Is de parsing simpel genoeg om uit te drukken als Migrate plugin, of heb je echt Feeds nodig? Verandert de bron in de komende 24 maanden?

Verdicten:

  • 4 importers bleven in Feeds. De contrib-module heeft een D8+/D11-port. CSV- en JSON-parsers gingen mee. We herschreven de veld-mappings.
  • 2 importers werden custom Migrate plugins. Het waren eenmalige XML-feeds met eigen veldlogica, makkelijker als kale Migrate-YAML dan als Feeds tamper-rules.
  • 1 importer is gewoon verwijderd. Hij trok data uit een SOAP-endpoint dat de gemeente in 2022 had uitgezet. Niemand bij de klant wist dat hij dood was.

De korte les voor elke Feeds-zware D7-site: ga niet uit van één-op-één. De Migrate API is inmiddels expressief genoeg om de helft van wat Feeds vroeger deed over te nemen, en je raakt meteen een contrib-dependency kwijt.

Een schone Drupal 11-host, geen in-place upgrade

Er is een school die zegt dat je de core-migratie in-place moet draaien. Drupal 11 over dezelfde database installeren, en drush migrate:import de content node voor node naar het nieuwe schema laten verplaatsen.

We hebben het gedaan. De eerste keer gaat het sneller, de tweede keer is het ellende. De D7-database draagt vijftien jaar legacy met zich mee: verweesde URL-aliases, taxonomy-termen die naar verwijderde vocabulaires verwijzen, file_managed-rijen die naar files wijzen die in 2019 van de disk zijn gevallen. Een in-place migratie sleept dat allemaal mee naar D11.

Een schone Drupal 11-installatie op een nieuwe host, die via een read-only verbinding uit de D7-database trekt, laat jou bepalen wat de overstap maakt.

composer create-project drupal/recommended-project:^11 utrecht-housing
cd utrecht-housing
composer require drush/drush:^13
composer require drupal/migrate_plus drupal/migrate_tools drupal/migrate_upgrade

In settings.php, de legacy connection:

$databases['migrate']['default'] = [
  'database' => 'utrecht_housing_d7',
  'username' => 'd7_readonly',
  'password' => '...',
  'host'     => '10.0.4.12',
  'driver'   => 'mysql',
  'prefix'   => '',
];

Read-only is niet optioneel. Sommige plugins kunnen terugschrijven naar de bron als je ze laat, en je wilt niet dat een mislukte import je enige werkende kopie corrumpeert.

De migratie in drie passes draaien

De volledige migratieset draait in ongeveer 1,5 uur op de productie-spec host. We splitsen 'm in drie groepen, zodat we kunnen stoppen, inspecteren en opnieuw draaien.

Pass één is users, taxonomy, file-metadata. Nog geen content.

drush migrate:import --group=d7_users
drush migrate:import --group=d7_taxonomy
drush migrate:import --group=d7_file_metadata

Pass twee is files en media. De D7-site gebruikte het oudere file-veld; D11 gebruikt Media-entities. We bouwden de brug met migrate_file_to_media uit contrib.

Pass drie is content, in dependency-volgorde. Eerst pagina's, dan artikelen, dan de property-nodes die naar beide verwijzen. Workbench Moderation-states hangen er in deze stap aan via de custom process plugin.

public function transform($value, MigrateExecutableInterface $executable, Row $row, $destination_property) {
  $map = [
    'draft'        => 'draft',
    'needs_review' => 'needs_review',
    'published'    => 'published',
    'archive'      => 'draft', // logged separately for editor review
  ];
  if (!isset($map[$value])) {
    throw new MigrateSkipRowException("Unknown D7 state: {$value}");
  }
  return $map[$value];
}

De MigrateSkipRowException is het werkpaard. Het is het verschil tussen een migratie die stilletjes faalt en een migratie die je precies vertelt welke rij je moet fixen.

Diff-scripts, geen QA met het oog

Niemand controleert 1.400 pagina's op gevoel. We schreven drie diff-scripts.

Count diff: per content type, per workflow-state, per taxonomy-term, het aantal in D7 vs het aantal in D11.

#!/usr/bin/env bash
for type in page article property news; do
  d7=$(drush @d7 sql:query "SELECT COUNT(*) FROM node WHERE type='$type'" | tail -1)
  d11=$(drush @d11 sql:query "SELECT COUNT(*) FROM node_field_data WHERE type='$type'" | tail -1)
  printf "%-12s d7=%5d  d11=%5d  diff=%d\n" "$type" "$d7" "$d11" "$((d7-d11))"
done

Body diff: haal de gerenderde HTML van elke node op D7-staging en D11-staging op, normaliseer whitespace en vergelijk. We accepteren themadrift en nieuwe wrapper-divs. Al het andere gaat naar review.

URL diff: elke D7-URL-alias moet op D11 resolven naar dezelfde entity, of 301'en naar een nieuw pad dat de klant heeft goedgekeurd. We hebben beide kanten gecrawld en vergeleken.

Op Utrecht ving de URL-diff 23 mismatches die het contentteam niet had gezien. Nodes die in D7 twee aliases hadden, één Nederlands en één Engels, waarbij alleen de Nederlandse meekwam naar D11. Zonder de diff waren die op launch day live gegaan als 404.

Wat we draaien op launch day

Een staging-naar-productie cutover is een eigen playbook. Kort: contentfreeze 48 uur van tevoren aangekondigd, 's ochtends onderhoudsmodus aan op D7, laatste migratie-delta, DNS-swap, redirect-map deployen, monitoring dashboard de eerste 24 uur op een tweede scherm.

Het kleinste wat je vandaag kunt doen: draai de audit van vijf regels uit de top van dit stuk tegen je eigen Drupal 7-site. Vind je een workflow-state die niemand zich herinnert te hebben geconfigureerd, of een Feeds-importer die niemand kan benoemen, dan weet je nu het duurste onderdeel van je migratie. Toen we deze rebuild bouwden voor de Utrechtse woningcorporatie, kostte het meeste tijd niet de content. Het waren de Feeds-triage en het opruimen van orphan-states. Dit soort werk doen we als legacy-migratie, en het playbook hierboven is ongeveer wat we draaien op elke D7-site die we oppakken.

Kern

Het dure deel van een Drupal 7-migratie is niet de content. Het zijn de verweesde states en de slapende Feeds-importers die niemand kan uitleggen.

FAQ

Is een in-place upgrade van D7 naar D11 ooit de juiste keuze?

Alleen voor kleine, schone sites onder de 100 nodes zonder contrib-moderation of Feeds-gebruik. Voor alles wat groter is, geeft een schone D11-installatie die uit een read-only D7-bron trekt je de kans om de rommel weg te snijden.

Migreert Workbench Moderation automatisch naar Content Moderation?

Nee. States en transities vereisen een handgebouwde Workflow config entity, plus een custom process plugin om state-waarden per revisie te mappen. Orphan states laten de import stilletjes mislukken.

Is de D7 Feeds-module één-op-één over te zetten naar D11?

Gedeeltelijk. De Feeds contrib-module heeft een D8+/D11-port, maar het config-schema is veranderd. Simpele CSV- en JSON-importers gaan mee. Eigen parsers herschrijf je vaak makkelijker als Migrate plugin.

Hoe lang duurt een migratie van 1.400 pagina's?

Ongeveer 1,5 uur op productie-spec hardware voor de volledige set. Splits 'm in drie passes (users/taxonomy, files/media, content), zodat je kunt stoppen, verifiëren en één groep opnieuw kunt draaien als iets misgaat.

Is Drupal 7 in 2026 nog veilig om te draaien?

Nee. End-of-life is op 5 januari 2025 verlopen. Er worden geen security-advisories meer uitgegeven, en contrib-modules ontvangen geen patches meer. Elke maand uitstel verhoogt de kosten van de volgende geforceerde fix.

drupalmigrationlegacy sitesphpcase studyarchitecture

Iets bouwen?

Start een project