Drupal
Drupal 11 migratie-audit: wat we checken voor de offerte
De audit die we op elke Drupal 8 of 9 site draaien voor we een Drupal 11 migratie offreren. Verlaten modules, theme-entropie, en vier config-sync valkuilen die ons vorige project een week werk kostten.

Een trouwe klant mailde ons in maart. Hun Drupal 9 site, een vakportaal dat we al twee jaar niet hadden aangeraakt, gooide opeens kritieke beveiligingswaarschuwingen op. Drupal 9 ging in november 2023 end-of-life. Ze wilden “een snelle offerte om hem naar 11 te tillen”. We zeiden nee. Nog niet. Eerst de audit.
Die mail is de reden dat dit stuk bestaat. Drupal 8-naar-11 offertes die de audit overslaan zijn de manier waarop studios twee weken ongefactureerd werk opslokken om dingen te fixen waarvoor ze geen prijs hadden gerekend. Dus voor we een getal sturen, draaien we dezelfde checklist op elke site. Het kost een dag. Het redt het project.
De audit gaat voor de offerte uit
Drupal 11 kwam uit in augustus 2024 en het upgradepad is op papier een non-event. Composer update, drush updb draaien, en de deprecations fixen die de rector niet heeft gevangen. In de praktijk buigen drie dingen dat pad uit z'n vorm:
- Contrib modules die niemand meer onderhoudt.
- Een custom theme dat drie minor versies is weggedreven van z'n base.
- De geëxporteerde configuratie van de site, vol met kleine leugens die ze over zichzelf vertelt.
De audit vangt alle drie. We draaien hem op een verse lokale kopie van de site, opgehaald met drush sql-sync en drush rsync vanuit productie, plus de volledige config export en een snapshot van composer.lock. Minder en je zit te gissen.
Verlaten modules
Het eerste wat we draaien is composer outdated, gevolgd door een grep voor elke contrib module in het project. Voor elke module openen we de drupal.org pagina en checken we drie signalen:
- Datum van de laatste commit. Alles ouder dan negen maanden is geel. Ouder dan achttien maanden is rood.
- Drupal 11 compatibiliteit. De projectpagina toont een groen vinkje of een “no stable release” waarschuwing.
- Trend in de issue queue. Een module met tachtig open issues en één merge in een jaar is stervende, ook al beweert de maintainer iets anders.
We zetten op elke module een van vier labels: behouden, vervangen, forken, of verwijderen. “Vervangen” betekent dat er een werkbare opvolger bestaat in core of in een gezonder contrib project. “Forken” betekent dat we hem zelf gaan patchen tijdens de migratie. “Verwijderen” is gereserveerd voor modules waarvan de feature al twee jaar stilletjes dood is en niemand het heeft gemerkt.
De Drupal community publiceert een upgrade guide die laat zien welke contrib projecten klaar zijn voor 11. Het is een prima startpunt, maar het scheidt niet tussen modules die 11-ready zijn omdat iemand er gisteren een patch op heeft gezet en modules die 11-ready zijn omdat ze al jaren bevroren zijn en toevallig nog compileren. We checken de commit history sowieso.
Het security team van Drupal schrijft geen advisories voor niet-ondersteunde modules. Houd je een verlaten module in productie, dan vlieg je zonder windzak. Vervang of fork hem voor de upgrade, niet erna.
De deliverable van dit deel is een platte CSV: modulenaam, actie, urenschatting, risico. Die gaat direct de offerte in.
Custom theme entropie
Drupal themes rotten niet zoals modules dat doen. Ze driften. Elke minor versie van Drupal core verscheept kleine Twig-wijzigingen, hernoemde attributen, aangepaste library handles. Een theme dat schoon was gebouwd tegen 8.6 heeft tegen 9.5 een stille schaduw aan deprecated calls opgebouwd die prima blijven renderen, tot het moment dat het niet meer zo is.
We meten entropie in drie passes.
Eerste pass: drupal-rector draaien tegen de theme-directory en deprecation hits tellen. Een theme onder de vijftig hits is gezond. Vijftig tot tweehonderd is een dag refactor. Boven de tweehonderd moet je overwegen of het theme niet opnieuw gebouwd moet worden tegen een actuele base theme in plaats van meegesleept.
Tweede pass: een grep op drupal_add_js, drupal_add_css, theme(), en directe db_query() calls. Die waren al weg in Drupal 8, maar ze sluipen erin via gekopieerde snippets uit oude antwoorden op Stack Exchange. Elk eentje is een kleine bom.
Derde pass: tel de preprocess functies. Themes met meer dan dertig preprocess functies doen meestal werk dat in een module thuishoort. De render pipeline van Drupal 11 is strenger over cacheability metadata dan die van 9 was. Preprocess functies die render arrays muteren zonder cache contexts te declareren breken page caching in stilte na de upgrade. We vlaggen ze allemaal.
// Waar we in de audit op letten. Pre-D8 calls die nog altijd opduiken in custom themes.
function THEME_preprocess_node(&$variables) {
$result = db_query(
"SELECT * FROM {node_field_data} WHERE nid = :nid",
[':nid' => $variables['node']->id()]
);
// Dit overleeft de upgrade niet. Vervang door een entity query en
// declareer fatsoenlijke cache tags op $variables.
}
De output is een entropie-score voor het theme en een aanbeveling: shippen, refactoren, of opnieuw bouwen. Refactor en herbouw zijn heel andere posten. De audit bepaalt welke.
Vier config-sync valkuilen die ons een week kostten
Dit is het deel waar de meeste offertes de mist in gaan. Config sync ziet er aan de oppervlakte deterministisch uit. drush config:export, commit, drush config:import aan de andere kant. In de praktijk vangen dezelfde vier valkuilen ons als we ze niet vooraf checken. Op het project dat deze checklist heeft uitgelokt, kostten ze ons een week aan ongepland werk.
1. UUID-botsingen op opnieuw aangemaakte entities
Elke config entity in Drupal heeft een UUID. Heeft iemand op enig moment een view, vocabulary, of content type verwijderd en opnieuw aangemaakt, dan komt de UUID in de database niet meer overeen met die in de geëxporteerde config. Importeren slaagt dan op onvoorspelbare manieren, of faalt, afhankelijk van welke omgevingen wanneer zijn geëxporteerd.
We greppen de geëxporteerde config op UUIDs en checken die kruislings tegen de live database met een kleine drush eval snippet. Elke mismatch komt op de lijst. Oplossen is mechanisch werk, maar het zijn uren mechanisch werk, en je moet ervan af weten voor je offreert.
2. Field storage versus field instance volgorde
field.storage.node.body.yml definieert de storage. field.field.node.article.body.yml definieert de instance. Importeer je field instances voor hun storage bestaat, dan valt de import halverwege uit. Drupal rolt niet netjes terug. Je houdt een half-geïmporteerde site over die handmatige opschoning nodig heeft.
We checken dit door een dry import te draaien tegen een schone D11-install met dezelfde modules aan, en we letten op order-of-operations errors. De fix is meestal een config_split tweak of een hook_install in een deploy-module. Hoe dan ook: twee tot vier uur die we nu standaard in elke offerte verwerken.
3. Default config die zichzelf opnieuw importeert
Modules verschepen default configuratie in hun config/install directory. Zet je een module aan, dan kopieert Drupal die default config naar de actieve store. Sommige sites passen die daarna aan. De aanpassingen leven in de actieve config, niet in de module. Is je config/sync directory gevuld vanuit een al lang dode omgeving, dan kunnen die aanpassingen ontbreken, en herstelt de import in stilte de defaults.
De signature van deze valkuil is “de config import slaagde, de site ziet er prima uit, en een week later merkt iemand dat een veldlabel terug op default staat.” Wij diffen config/install tegen de actieve config voor elke aangezette module en vlaggen alle drift.
4. Config die environments laat lekken
API keys, debugging flags, file paths, mail aliases. Niets daarvan hoort in synced config. Het belandt er allemaal vroeg of laat in, meestal omdat iemand een module heeft aangezet in productie, hem via de UI heeft geconfigureerd, en geëxporteerd. De resulterende YAML heeft nu een Mailgun API key erin. Of een pad naar /var/www/staging. Of een debug flag op true.
We greppen de export op de voor de hand liggende markers: api_key, secret, staging, localhost, 127.0.0.1, xdebug, en alles wat base64-vormig oogt. Elke hit wordt of een config_split kandidaat, of, voor echte secrets, een verhuizing naar environment variables via settings.php.
De versie van een half uur
Heb je geen dag, dan kun je de offerte alsnog in een half uur ontrisken. Open een terminal in een verse clone van de site en draai:
# Module health snapshot.
drush pm:list --status=enabled --format=json | jq -r '.[].name' > enabled-modules.txt
# Composer outdated, alleen contrib.
composer outdated 'drupal/*'
# Aantal deprecations via de rector.
vendor/bin/rector process modules/custom themes/custom --dry-run | grep -c 'would have'
# Config drift check.
drush config:status
# Secrets in synced config.
grep -rE 'api_key|secret|password|token' config/sync/
Vijf commando's, drie koffie-minuten leeswerk, en je weet of de migratie een dinsdag is of veertien dagen.
Wat dit verandert aan de offerte
Offertes die uit deze audit komen hebben drie posten die de slechte offertes niet hebben: een budget voor module-vervanging, een entropie-score voor het theme, en een config-remediation blok. Ze bevatten ook een appendix “dingen die we hebben gevonden die je sowieso zou moeten fixen, los van de migratie”. Dat is meestal de reden waarom de klant besluit het werk aan ons te geven.
Toen we deze audit eerder dit jaar draaiden op de Drupal 9 site van een Nederlandse B2B uitgever, waren die vier config-sync valkuilen het regeltje dat de deal sloot. De vorige studio had veertig uur geoffreerd en de audit overgeslagen. Wij offreerden tachtig en lieten de CSV zien. De uitgever koos ons. Het werkelijke project landde op tweeënzeventig uur. Dat is het verschil dat een audit maakt. Werk je aan een vergelijkbaar traject, dan beschrijven onze notities over legacy migraties hoe we het werk structureren zodra de audit binnen is.
Het kleinste wat je vandaag kunt doen is het blok van vijf commando's hierboven draaien tegen je eigen Drupal site en kijken wat eruit valt. Komt de secrets-grep op meer dan nul hits, dan heb je een middag opschoonwerk voor de boeg, ongeacht of je ooit migreert.
Kern
Een Drupal 11 offerte zonder audit is een gok. Eén dag gestructureerd checken bespaart je de week die de config-sync valkuilen anders opslokken.
FAQ
Hoe lang duurt de volledige audit?
Een gemiddelde Drupal 9 site kost één engineer-dag. De versie van een half uur aan het eind van het stuk is een sanity check voor de offerte, maar die vangt de vier config-sync valkuilen niet.
Mag ik de audit overslaan als mijn Drupal 9 site klein is?
Onder de vijftien contrib modules en geen custom theme is de snelle check met vijf commando's meestal genoeg. Alles wat groter is, en de config-sync valkuilen vinden je in de derde week van het project.
Wat is de goedkoopste manier om met een verlaten contrib module om te gaan?
Fork hem naar je eigen Composer repository en patch alleen wat de upgrade blokkeert. Feature parity opnieuw schrijven in een andere module is bijna altijd duurder dan een kleine fork onderhouden.
Wanneer is Drupal 10 end-of-life?
Drupal 10 wordt naast Drupal 11 ondersteund tot minstens 2026, inclusief security updates. De huidige upgrade-druk ligt op sites die nog op 8 of 9 draaien, beide al voorbij end-of-life.