← Blog

Drupal

Drupal 7 naar Medusa 2: cutover bij een kaasexporteur

Een 14 jaar oude Drupal 7-webshop met Commerce Kickstart 2, 4.200 douaneklasse-artikelen en een nachtelijke Exact Globe-export. Zo zetten we 'm over zonder een certificaat te verliezen.

Jacob Molkenboer· Oprichter · A Brand New Company· 2 nov 2025· 9 min
Open leren douaneboek, koperen scheepslabel aan linnen touw, groen tabblad, stempel, inktkussen, koperen sleutel op ivoor papier.

Het is vrijdagavond in Mechelen. De exportcoördinator van een speciaalkaashandel met 24 mensen klikt op verzend bestelling en staart naar een wit scherm. De douane-PDF is half gegenereerd. De nachtelijke Exact Globe-run begint over twee uur. Een Drupal 7-module die sinds 2012 EU-exportcertificaten rendert, gooit net een undefined index-melding tegen een PHP 8.2-backport waarvan niemand zich herinnert dat hij ooit is toegepast.

De site is veertien jaar oud. Hij draait op Commerce Kickstart 2, een distributie die jaren geleden is gearchiveerd. Hij draagt 4.200 artikelen, elk gekoppeld aan een douaneklasse, elk goed voor een ondertekend EU-exportcertificaat, elk uiteindelijk een fixed-width grootboekregel in Exact Globe om 02:00 elke nacht. De kaas verlaat België bij zonsopgang. Niets daarvan kan langer dan een paar uur stuk zijn.

Dit is de playbook van vijf weken waarmee we die stack hebben overgezet naar Medusa 2 en Astro, met shadow traffic die de hele rit meeliep, zonder één douanecertificaat te verliezen.

Waarom verhuizen, en waarom niet naar een nieuwere Drupal

Drupal 7 bereikte community end of life op 5 januari 2025. Er zijn betaalde leveranciers die extended support aanbieden, en die hebben we eerder ingezet om een jaar te kopen. De klant had dat jaar al gekocht. Het echte probleem was niet Drupal zelf. Het was Commerce Kickstart 2, de custom Rules-logica, de zeventien contributed modules zonder Drupal 10-equivalent, en een TCPDF-certificaatgenerator die niemand in het team kon lezen.

Een like-for-like rebuild op Drupal 11 is doorgerekend. Het kwam op ongeveer dezelfde uren uit als de headless variant, met hetzelfde datawerk, maar met een CMS dat het operationele team niet meer nodig had. De site heeft zes redacteuren. Drie daarvan komen alleen aan productteksten. Niemand schrijft blogposts. Drupal droeg gewicht dat de business niet meer vroeg.

Medusa 2 werd de commerce-engine, Astro werd de storefront, en de redacteuren kregen een klein custom admin in het Medusa-dashboard voor de stukken waar ze daadwerkelijk aankwamen.

De datamodelkeuze waar al het andere aan hangt

In de oude site was de douaneklasse een taxonomy-term die werd gerefereerd vanuit een productvariant. Douanedocumentatie leefde als node reference. Het exportcertificaat-template leefde als een node met velden en een TCPDF-renderhook. Niets daarvan is ongewoon voor D7 plus Commerce. Alles ervan is broos.

Voordat we één regel Medusa-code schreven, hebben we de canonical objecten op papier gemodelleerd:

  • Artikel: Medusa Product, één per SKU.
  • Douaneklasse: een first-class tabel, geen metadata. Elk artikel linkt aan één douaneklasse met HS-code, gewichtsbasis en oorsprongsregel.
  • Exportcertificaat: een gegenereerd artefact in object storage met een signed URL, gekoppeld aan een orderregel.
  • Grootboekregel: een afgeleid record, op elk moment herbouwbaar uit de order.

De douaneklasse in een eigen tabel zetten voelde als een kleinigheid. Het redde ons in week vier, toen de auditor vroeg om de wijzigingsgeschiedenis van HS-code 0406.10.50 over de afgelopen drie jaar. Metadata-velden dragen geen geschiedenis. Een tabel met een versioned join wel.

Week 1: inventarisatie, geen code

We hebben in week één geen code-editor geopend. We zaten naast de exportcoördinator en liepen elk scherm door waar zij aankwam. We keken hoe ze een certificaat printte, een typo met de hand corrigeerde en de PDF opnieuw opsloeg. We keken hoe ze een order annuleerde waarvan al een gedeeltelijke pallet was verstuurd. We keken hoe ze een artikel opzocht op de Nederlandse naam, en daarna nog eens op de oude Belgische interne code.

De output van week één was één Markdown-bestand: 47 workflows, elk gekoppeld aan een specifieke URL op de oude site, elk getagd met een frequentie. Die frequenties deden ertoe. Certificaatgeneratie liep 60 keer per dag. Artikelduplicatie liep 4 keer per maand. We wisten waar we de engineering moesten neerleggen.

Week 2: de PDF-keten

Het exportcertificaat is geen marketing-PDF. Het is een juridisch document. De Belgische FAVV verwacht een specifieke layout, specifieke velden en een gekwalificeerde elektronische handtekening onder eIDAS. De oude D7-module riep TCPDF direct aan, mergde velden uit de order, schreef het bestand naar een lokale mount en mailde het. De ondertekenstap was handmatig en gebeurde aan het einde van de week vanaf iemands laptop.

We hebben dat opgeknipt in een schone keten:

// medusa/src/workflows/certificates/issue-export-certificate.ts
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { renderCertificatePdfStep } from "./steps/render-pdf"
import { signWithEidasStep } from "./steps/sign-eidas"
import { storeCertificateStep } from "./steps/store"

export const issueExportCertificateWorkflow = createWorkflow(
  "issue-export-certificate",
  (input: { order_id: string }) => {
    const html = renderCertificatePdfStep(input)
    const signedPdf = signWithEidasStep(html)
    const stored = storeCertificateStep({
      order_id: input.order_id,
      pdf: signedPdf,
    })
    return new WorkflowResponse(stored)
  }
)

Elke stap is idempotent en herafspeelbaar. De render-stap gebruikt een HTML-template en Playwright. De ondertekenstap roept een Belgische qualified trust service provider aan. De storage-stap schrijft naar S3-compatible object storage met een deterministische key. Als de signing-service om 14:00 onderuit ligt, houdt de queue de job vast, vertrekt de order zodra de handtekening om 14:18 binnenkomt, en merkt niemand op de werkvloer er iets van.

Let op

Hergenereer certificaten nooit bij read. De PDF die met een pallet meegaat moet byte-identiek zijn aan de PDF die bij de order is opgeslagen. We hashen de gerenderde PDF, slaan de hash op en weigeren te re-renderen als er al een hash bestaat.

Week 3: de Exact Globe-nightly

De grootboekexport is een fixed-width tekstbestand. Het formaat is sinds 2011 ongewijzigd. Exact Globe parseert het met een handgeschreven Pascal-import. Niemand gaat die Pascal herschrijven. Het bestand moet eruit komen, teken voor teken, in dezelfde vorm.

We schreven een Medusa-subscriber die luistert naar order.completed, de grootboekregel materialiseert en wegschrijft naar een dagbestand:

// medusa/src/subscribers/grootboek-line.ts
import { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"

export default async function grootboekLineHandler({
  event,
  container,
}: SubscriberArgs<{ id: string }>) {
  const ledger = container.resolve("grootboekService")
  await ledger.appendLineForOrder(event.data.id)
}

export const config: SubscriberConfig = {
  event: "order.completed",
}

Om 01:55 elke nacht roteert een scheduled job het bestand, valideert het tegen een golden fixed-width schema en sftp't het naar de share die Exact Globe in de gaten houdt. De validator is het belangrijkste stuk. De oude site kortte lange artikelomschrijvingen stilzwijgend af op 30 tekens. De nieuwe site weigert het bestand te schrijven als één regel niet klopt, en pieptert de on-call engineer. In vijf maanden draaien heeft de validator twee issues gevangen. Beide waren onze fout. Beide werden gevangen voordat Exact Globe ze zag.

Week 4: shadow traffic

Dit is de week die de rest terugverdient. We zetten een kleine Cloudflare Worker voor het oude domein. Elke read-request werd geserveerd door de oude D7-site. Elke read werd ook, asynchroon, gespiegeld naar de nieuwe Medusa- en Astro-stack op een staging-hostname. De responses werden naast elkaar opgeslagen. Een diff-job liep elk uur.

Wat we vonden in week vier, in ruwe volgorde:

  1. Btw-afronding op EU-export verschilde één cent op ongeveer 0,4% van de orders. De oude site rondde per regel af. De nieuwe site rondde het totaal af. We hebben Medusa aangepast naar afronden per regel.
  2. De oude search behandelde brie en brié als verschillende termen. De nieuwe search gebruikte een Postgres trigram-index die ze samenvouwde. De exportcoördinator wilde ze gescheiden houden, omdat brié een interne code was voor een gerijpte variant. We hebben een exact-match boost toegevoegd.
  3. De oude PDF gebruikte Helvetica. De nieuwe PDF gebruikte Inter. De FAVV-portal weigert Inter omdat de OCR is getraind op Helvetica. We zijn teruggegaan.

Geen van die drie was uit een checklist gerold. Shadow traffic vindt ze, omdat echte klanten het systeem gebruiken in vormen die niemand heeft gedocumenteerd. We schreven in week vier geen enkele nieuwe geautomatiseerde test. We schreven drie nieuwe validators en lieten het live verkeer de testcases produceren.

Week 5: cutover en de long tail

De DNS-flip is het saaie stuk. De TTL was drie weken eerder al verlaagd naar 60 seconden. We flipten de CNAME op een dinsdag om 09:30, keken hoe het read-verkeer verschoof en hielden de oude stack warm. De shadow-worker draaide om: de nieuwe stack serveerde, de oude stack ontving 14 dagen lang gespiegelde writes, en we hadden replay-scripts paraat voor het geval er iets misging.

Op de ochtend van de flip ging er niets mis. In de twee weken erna gingen drie dingen mis, en alle drie waren saai:

  • Een scheduled robot van een Nederlandse B2B-portal hamerde nog op een oude XML-feed-endpoint. Die hebben we op een 301 gezet naar de nieuwe feed.
  • Eén grote klant had een hardcoded order-email die in een Drupal Webform liep. We hebben die mail naar een parser geleid die in de nieuwe Medusa API pusht.
  • Twee vooruitbetaalde facturen uit het laatste uur van de oude stack waren niet naar Exact Globe gepusht. Het replay-script handelde ze in 90 seconden af.

Vijf weken betaalde aandacht, gefactureerd tegen vaste prijs. Een veertien jaar oude shop landde op een stack waar het team voor kan rekruteren. De douaneklasse-tabel is voor het eerst in tien jaar auditable, de redacteuren zijn opgehouden met vechten tegen de WYSIWYG, en geen enkel exportcertificaat is verloren gegaan.

Wat we je vandaag zouden aanraden

Zit je op Drupal 7 met een werkende webshop, dan is de verleiding om te wachten. Wachten tot extended support afloopt. Wachten tot een security-patch verkeerd landt. Wachten tot de developer die de douanemodule kende met pensioen gaat. Niet wachten. Besteed een week aan het in kaart brengen van elke workflow op papier, zoals wij in week één deden, voordat iemand code schrijft. Die kaart vertelt je of je een maand of een kwartaal werk hebt, en hij vertelt je of headless het juiste antwoord is of dat een rebuild op het huidige Drupal het is.

Toen we de headless cutover voor de Mechelse kaasexporteur bouwden, kwamen we steeds terug op één punt: de douane-PDF-keten was de dragende muur, niet de storefront. De storefront was makkelijk. De PDF-keten was waar de business zat. Kijk je naar een vergelijkbare legacy migratie, vind dan eerst je dragende muur.

Het kleinste wat je vandaag kunt doen: open je oude admin, klik door één volledige end-to-end order en schrijf elk scherm op dat je hebt aangeraakt. Wordt die lijst langer dan negen, dan heb je een Mechelen-achtige klus.

Kern

Bij een veertien jaar oude Drupal 7-webshop is de douane-PDF-keten de dragende muur, niet de storefront. Breng elke workflow op papier in kaart voordat je één regel code schrijft.

FAQ

Waarom niet gewoon upgraden naar Drupal 11?

We hebben het doorgerekend. Dezelfde uren als headless, door Commerce Kickstart 2 en 17 contributed modules zonder D10-equivalent. Zes redacteuren, drie daarvan komen alleen aan productteksten. Het CMS droeg gewicht dat niemand gebruikte.

Hoe risicovol is shadow traffic als de writes uit elkaar lopen?

Alleen reads werden gespiegeld. Writes draaiden tot de cutover alleen op de oude stack, daarna alleen op de nieuwe, waarbij de oude stack 14 dagen lang gespiegelde writes ontving als vangnet. Geen dubbele facturatie, geen dubbele verzending.

Hoe lang duurde de cutover in kalendertijd?

Vijf weken. Week één was inventarisatie op papier, geen code. Week twee en drie bouwden de PDF-keten en de Exact Globe-sync. Week vier draaide shadow traffic. Week vijf flipte DNS en hield de oude stack warm.

En SEO tijdens de migratie?

301-redirects van elke oude URL naar de nieuwe, sitemap ingediend op de flipdag, structured data-pariteit gecheckt via de diff-job. De rankings op de top 200 productpagina's bleven binnen één positie.

drupalmigrationlegacy sitese-commercecase studyarchitecture

Iets bouwen?

Start een project