— Artikel — № 113

113 —Workflow

Statamic-migratie: het vijfdaagse content-freeze draaiboek

Gefaseerd content-freeze draaiboek voor een vijfdaagse Statamic-cutover: redactionele lock, drie diff-checkpoints en de .htaccess die meegaat.

Foto van boven op linnen: freeze-kalender, drie diff-vellen, .htaccess-blad, schets op ruitjespapier, koperen label, lakzegel, vulpen.
Hero · gestileerd stilleven№ 113

Zondagavond, 23:41. Een bureau-eigenaar uit Eindhoven stuurde een Loom. Haar vijfdaagse Statamic-migratie zou de volgende ochtend cutover gaan, en een redacteur had haar net in paniek een bericht gestuurd. Drie posts die de redacteur op vrijdagmiddag had gepubliceerd, nadat het bureau de baseline-export had genomen, stonden nergens in de staging-build. De 301-map was al dichtgetimmerd. De DNS-TTL stond al op 60 seconden. En de content freeze hield op papier al sinds woensdag stand.

Dit is het faalpatroon waar elke Statamic-migratie vroeg of laat tegenaan loopt. Eén harde freeze behandelt een redactieteam als een database, en redactieteams zijn geen databases. Ze hebben dinsdag een vergadering, donderdag een campagne, en een persbericht dat de deur uit moet voordat het embargo opheft. Een gefaseerde freeze, met diff-checkpoints en één smalle publicatiewindow, laat de migratie afronden zonder stilletjes het werk te slopen dat in de gaten valt.

Hieronder staat het draaiboek dat we gebruiken voor een vijfdaagse cutover van een verouderde site (meestal WordPress, soms Drupal) naar Statamic. Echte commando's, echte bestandspaden, en echte concessies aan het feit dat het redactieteam zijn eigen deadlines heeft.

Waarom één harde freeze niet werkt

De boekenwijsheid: bevries het bron-CMS op dag één, migreer de content, cutover op dag vijf. Het faalt om drie redenen die je pas op dag zes merkt.

Eén: in redactionele workflows zitten beloftes ingebakken. Een post die voor woensdag staat ingepland, kun je niet verplaatsen zonder uitleg aan degene die hem heeft ingepland. Tegen donderdag heeft iemand stiekem unpublished en gerepubliceerd om de freeze te omzeilen, en zit de post in je baseline-export met een ander ID dan de live versie.

Twee: het technische team behandelt de freeze als een checkpoint en het redactieteam behandelt het als een verzoek. Tegen dag drie heeft iemand een concept gepubliceerd, een gepubliceerde post bewerkt, of een hero-image van 47 MB geüpload naar wp-content/uploads/2026/06/ die niet in je laatste rsync zit.

Drie: de diff tussen wat je hebt geëxporteerd en wat live staat, groeit lineair met de duur van de freeze. Na 24 uur is het met de hand te verzoenen. Na vijf dagen niet meer.

De gefaseerde freeze keert dit om. In plaats van één blackout neem je drie snapshots en één korte publicatiewindow, en de diff zelf wordt het artefact dat je migreert.

Dag min één: aankondigen, dan snapshotten

De aankondiging is belangrijker dan de snapshot, omdat de snapshot mechanisch is en de aankondiging politiek. Stuur hem 48 uur voordat de freeze begint, naar een benoemde lijst, met drie dingen erin: het exacte tijdvenster waarin publiceren is geblokkeerd, de naam van de persoon die een uitzondering kan toestaan, en de URL van een gedeeld formulier voor posts die tijdens de window absoluut live moeten. Stuur geen Slack-bericht. Mensen scrollen door Slack heen.

Zodra de mail is verstuurd, neem je de baseline. Voor een WordPress-bron zijn dat drie artefacten:

wp export --dir=/tmp/baseline --skip_comments
mysqldump --single-transaction --quick \
  --databases wp_prod \
  -u root -p > /tmp/baseline/wp_prod.sql
rsync -av --delete \
  /var/www/html/wp-content/uploads/ \
  ./baseline/uploads/

Commit de baseline-directory in een private git-repo. Git is de diff-tool die je op dag drie nodig gaat hebben. Tag de commit baseline-day-0. Sla deze stap niet over. De verleiding om snapshots als gedateerde mappen te bewaren is groot, en gedateerde mappen maken een three-way diff lastiger dan nodig.

Voor Drupal-bronnen vervang je dit door drush ard of een node-export module plus een files/ rsync. Voor Joomla dump je de database en rsync je images/. De vorm blijft hetzelfde: een database-dump, een content-export en de geüploade assets, allemaal in één gecommit boom.

Dag één en twee: bouw eerst de doelstructuur, daarna pas content

De fout op dag één is direct beginnen met posts overzetten. De juiste stap is eerst de Statamic-structuur opzetten: blueprints, collections, taxonomies, asset containers. Het transform-script dat een WordPress-post inleest en een Statamic-entry wegschrijft, heeft het doelschema nodig, en in dat doelschema zitten de meeste redactionele beslissingen verstopt.

Een minimale blueprint voor een blogpost op Statamic 5 ziet er zo uit:

title: Post
tabs:
  main:
    sections:
      - fields:
          - handle: title
            field: { type: text, required: true }
          - handle: hero
            field: { type: assets, container: hero, max_files: 1 }
          - handle: body
            field: { type: bard, save_html: false }
          - handle: legacy_id
            field: { type: integer, listable: hidden }

Het legacy_id-veld is degene die de meeste teams in de eerste ronde vergeten en bij de derde betreuren. Het is de enige betrouwbare join key tussen de WordPress-post die vandaag bestaat en de Statamic-entry die morgen bestaat. Zonder dat veld is de diff op dag drie onmogelijk.

Het transform-script leest dan de WXR-export, loopt elk item langs, en schrijft een Markdown-bestand met YAML front matter weg in content/collections/posts/. Houd het idempotent. Bij opnieuw uitvoeren moet het overschrijven, niet aanvullen, want je gaat het drie keer draaien voor vrijdag.

De asset-rewrite is waar het misgaat

Inline-afbeeldingen in WordPress-posts verwijzen naar /wp-content/uploads/2024/03/hero.jpg. Die URL's geven een 404 op de nieuwe site, tenzij je een van twee dingen doet. De nettere optie: schrijf elke inline-verwijzing tijdens de transform om naar het /assets/-pad van Statamic en kopieer de bestanden naar de nieuwe asset container. De snellere optie, prima voor kleinere sites: houd /wp-content/uploads/ live op de nieuwe server als statische directory en voeg een Apache-regel toe die hem ongemoeid serveert:

<Directory /var/www/statamic/public/wp-content>
    Options -Indexes
    Require all granted
</Directory>

De nettere optie is wat je op de lange termijn wilt. De snellere optie is wat je dinsdag uitrolt, zodat de diff van woensdag iets betekent.

Dag drie: de diff bepaalt wat live gaat

Woensdagochtend voer je de baseline-capture opnieuw uit in een tweede directory en tag je hem baseline-day-3. Daarna draai je git diff tussen de twee tags, maar alleen tegen de database-dump en de content-export. De diff op uploads is ruis: redacteuren voegen continu featured images toe en bijna niets ervan doet ertoe.

git diff baseline-day-0 baseline-day-3 -- '*.sql' '*.xml' \
  | grep -E '^(\+|-)' \
  | grep -v '^(---|\+\+\+)'

Je krijgt drie categorieën wijzigingen terug. Nieuwe posts die sinds maandag zijn gepubliceerd. Bewerkingen op posts die al in de baseline zaten. Metadata-updates: gewijzigde slugs, nieuwe categorieën, aanpassingen aan de homepage-layout. Pak ze in die volgorde aan.

Voor nieuwe posts draai je het transform-script opnieuw tegen de day-3-export. Het legacy_id-veld zorgt dat de nieuwe entries op de juiste plek in content/collections/posts/ landen, zonder iets te dupliceren dat al gemigreerd is.

Voor bewerkingen heb je een keuze. Of je transformeert de bewerkte posts opnieuw en overschrijft hun Statamic-entries, of je accepteert dat die bewerkingen verloren gaan en meldt het aan de redacteur. Wij transformeren bijna altijd opnieuw. Het script staat er al, het op een subset draaien is goedkoop.

Voor metadata: met de hand. Slug-wijzigingen krijgen een regel in je redirect-map. Hernoemde categorieën vragen om een blueprint-update. Homepage-layout-wijzigingen vragen om een redactioneel gesprek, want Statamic en WordPress modelleren homepages niet hetzelfde, en doen alsof het wel zo is, is precies hoe je een launch met een kapotte hero uitrolt.

Dag vier: de zachte freeze met één publicatiewindow

Donderdag is de zwaarste dag voor het redactieteam. De harde freeze begint om 09:00 en duurt 24 uur. Maar omdat je hem 48 uur van tevoren hebt aangekondigd, en omdat er een benoemd uitzonderingsproces is, geef je één window terug: donderdag 16:00 tot 17:00, met de regel dat alles wat in die window wordt gepubliceerd in een ledger wordt opgenomen.

De ledger is letterlijk een Google Sheet met vier kolommen: URL, auteur, published_at, en een vinkje voor ported. Tijdens de window polt je transform-script elke vijf minuten de WordPress REST API:

curl -s "https://oldsite.example/wp-json/wp/v2/posts?after=2026-06-11T16:00:00&per_page=20" \
  | jq '.[] | {id, slug, title: .title.rendered}'

Elke post die terugkomt, wordt getransformeerd en aan de Statamic-build toegevoegd. De auteur krijgt een Slack-bericht dat de post het heeft gehaald. Het vinkje gaat aan. Om 17:01 wordt de zachte freeze een harde freeze, en de diff van vrijdagochtend zou leeg moeten zijn.

Dag vijf: cutover, redirects en de 404-audit

De DNS-TTL staat al op 60 seconden, dinsdag verlaagd. Vrijdagochtend is de volgorde: laatste transform, laatste build, upstream omzetten, 404's in de gaten houden.

De 301-map is het artefact dat het langst duurt om goed te krijgen en het kortst om uit te rollen. Voor de meeste WordPress-sites is de regelset klein genoeg om in .htaccess op de nieuwe server te passen:

RewriteEngine On

# Old WordPress dated permalinks to Statamic flat slugs
RewriteRule ^([0-9]{4})/([0-9]{2})/([0-9]{2})/([^/]+)/?$ \
  /blog/$4 [R=301,L]

# Author archives are gone, send them to /blog
RewriteRule ^author/[^/]+/?$ /blog [R=301,L]

# Old uploads stay where they are
RewriteCond %{REQUEST_URI} ^/wp-content/uploads/
RewriteRule ^ - [L]

# Feed compatibility for subscribers
RewriteRule ^feed/?$ /feed.xml [R=301,L]

Test de redirect-regels tegen een lijst van de top 500 URL's uit Google Search Console voordat je de cutover doet, niet erna. De documentatie van Apache mod_rewrite is de canonieke referentie en de moeite waard om opnieuw te lezen elke keer dat je een regel met een regex-backreference schrijft. Het eerste uur na cutover is wanneer crawlers elke redirect-regel testen die je hebt geschreven, en dat doen ze parallel.

Draai de eerste 48 uur elke twee uur een 404-audit. De snelste manier is het access-log tailen en de misses bucketen:

tail -F /var/log/apache2/access.log \
  | awk '$9 == "404" {print $7}' \
  | sort | uniq -c | sort -rn | head -20

Alles in de top 20 met meer dan 50 hits in twee uur krijgt een redirect-regel. Alles daaronder kan tot maandag wachten.

Version history als vangnet

Het laatste stuk van het draaiboek is geen stap, maar een houding. Elke wijziging aan de nieuwe site tussen dag één en dag vijf moet herstelbaar zijn. De Statamic-content-tree zit al in git, dus dat deel is gratis. Het .htaccess-bestand hoort ook in git. De MySQL-database die het dynamische werk doet, krijgt een snapshot vlak voor de cutover en nog één om 17:00 op vrijdag.

Toen we Pier bouwden, liepen we bij Statamic-migraties zoals deze steeds tegen hetzelfde aan: het team moet snel kunnen zien wat er tussen twee tijdstippen op de live server is veranderd, en één specifieke wijziging kunnen terugdraaien zonder de hele site terug te rollen. Dat hebben we uiteindelijk opgelost met een ingebouwde version history die elk opgeslagen bestand en elke database-write snapshot, met een one-click undo per regel, zodat de Loom van 23:41 een fix van vijf minuten wordt in plaats van een zaterdag in mysql.

Eén ding dat je vandaag kunt doen

Heb je een Statamic-migratie in de agenda staan? Schrijf vanmiddag de freeze-aankondiging en stuur hem 48 uur voor dag één. Al het andere in dit draaiboek hangt ervan af dat die mail vroeg genoeg bij de redacteuren in de inbox ligt om het uitzonderingsproces als een service te laten voelen, niet als een omweg.

— Vragen —

Hoe kort kan de redactionele freeze realistisch zijn?

24 uur, van donderdag 09:00 tot vrijdag 09:00, met daarbinnen een publicatiewindow van één uur. Korter en de cutover-diff valt niet meer te verzoenen.

Wat als het redactieteam helemaal niet wil bevriezen?

Draai de diff-scripts van dag drie en dag vier elk uur door tot aan de ochtend van de cutover en voer de wijzigingen direct in de transform. Langzamer en luidruchtiger, maar het werkt.

Moet ik WordPress-reacties meenemen?

Meestal niet. Op de meeste sites krijgt minder dan 1% van de posts in het laatste jaar reacties. Exporteer ze naar een statisch archief en link ernaar vanuit de post-footer in plaats van ze in Statamic over te zetten.