Migreren van de ene WordPress‑site naar de andere klinkt simpel, maar wie het ooit heeft gedaan weet:
het kan een tijdrovende, foutgevoelige klus zijn. Daarom kiezen steeds meer ontwikkelaars voor automatisering bijvoorbeeld met een Python‑script dat posts, media en metadata volledig automatisch overzet.
Maar is dat altijd de beste keuze? En wanneer is handmatig migreren juist slimmer?
In dit blog neem ik je mee door beide kanten.
Waarom WordPress‑migratie automatiseren met Python
1. Snelheid bij grote hoeveelheden content
Heb je honderden of duizenden posts? Dan is handmatig migreren simpelweg geen optie. Een script haalt posts op via de WordPress REST API, uploadt media opnieuw en zet alles netjes over — zonder dat jij uren hoeft te klikken.
2. Minder menselijke fouten
Copy‑paste is foutgevoelig. Een script doet elke post op exact dezelfde manier.
Dat betekent: consistente titels, content, metadata en featured images.
3. Perfect voor herhaalbare migraties
Werk je met staging‑omgevingen of meerdere WordPress‑installs?
Dan is een script goud waard. Eén druk op de knop en je content staat waar je ’m wilt hebben.
4. Je kunt het uitbreiden zoals je wilt
Wil je ook tags, categorieën, custom fields of zelfs WooCommerce‑producten meenemen?
Python geeft je volledige controle.
Maar… automatiseren is niet altijd de beste keuze
Hoewel een script krachtig is, zijn er situaties waarin handmatig migreren juist verstandiger is.
1. Je oude site is rommelig
Oude shortcodes, kapotte embeds, page‑builder‑restanten, dubbele media… Een script kopieert dat allemaal blind mee. Handmatig migreren geeft je de kans om meteen op te schonen.
2. Je wil de content toch nalopen
Veel bedrijven gebruiken een migratie als moment om:
- SEO‑titels te verbeteren
- afbeeldingen te optimaliseren
- categorieën te herstructureren
- oude content te verwijderen
Dat gaat beter met menselijke ogen.
3. Je hebt veel custom post types of plugin‑data
Sommige plugins gebruiken eigen tabellen of API‑routes.
Een script moet je dan uitbreiden, en dat kost tijd. Soms is handmatig sneller.
4. Je site is klein
Heb je minder dan 50 posts?
Dan is handmatig migreren vaak sneller dan debuggen.
De ideale aanpak: een hybride strategie
De beste migraties combineren het beste van beide werelden:
- Automatisch voor bulkcontent (posts, media, categorieën)
- Handmatig voor kwaliteitscontrole en opschoning
Zo voorkom je fouten én houd je controle.
Hoe ziet een robuust migratiescript eruit?
Een goed script bevat:
- retry‑mechanismen
- logging
- batch‑verwerking
- media‑upload met foutafhandeling
- validatie per post
- throttle om rate‑limiting te voorkomen
Hiermee verklein je de kans op fouten tot bijna nul.
Het script staat hieronder maar heb ik nog niet getest en is een eerste versie gebouwd door Copilot.
Conclusie: automatiseren is handig — maar niet blindelings
Een Python‑script kan je uren werk besparen en een migratie veel betrouwbaarder maken.
Maar automatiseren is geen doel op zich.
Soms is handmatig migreren juist slimmer, vooral als je content wilt opschonen of als je site complex is.
De beste migratie is degene die:
- snel genoeg is
- betrouwbaar genoeg is
- en jou de controle geeft die je nodig hebt
# =========================# VOORBEELD MIGRATIE SCRIPT IN PYTHON# =========================import requestsimport timeimport loggingfrom typing import Optional, Dict, Any, List# =========================# CONFIG# =========================SOURCE_BASE_URL = "https://oude-site.nl"DEST_BASE_URL = "https://nieuwe-site.nl"# Application Passwords of Basic AuthSOURCE_AUTH = ("source_user", "source_app_password")DEST_AUTH = ("dest_user", "dest_app_password")# Migratie-instellingenBATCH_SIZE = 10 # aantal posts per batchREQUEST_TIMEOUT = 15 # secondenMAX_RETRIES = 3RETRY_SLEEP = 3 # seconden tussen retriesSLEEP_BETWEEN_CALLS = 0.5 # throttle om rate limiting te voorkomenLOG_FILE = "wp_migratie.log"# =========================# LOGGING# =========================logging.basicConfig( filename=LOG_FILE, level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s",)logger = logging.getLogger(__name__)# =========================# HULPFUNCTIES# =========================def request_with_retry( method: str, url: str, auth=None, params: Optional[Dict[str, Any]] = None, json: Optional[Dict[str, Any]] = None, files: Optional[Dict[str, Any]] = None,) -> requests.Response: """HTTP-call met retries en logging.""" for attempt in range(1, MAX_RETRIES + 1): try: resp = requests.request( method=method, url=url, auth=auth, params=params, json=json, files=files, timeout=REQUEST_TIMEOUT, ) if 200 <= resp.status_code < 300: return resp else: logger.warning( f"HTTP {resp.status_code} bij {url} (poging {attempt}/{MAX_RETRIES}) - body: {resp.text[:500]}" ) except requests.RequestException as e: logger.error(f"RequestException bij {url} (poging {attempt}/{MAX_RETRIES}): {e}") if attempt < MAX_RETRIES: time.sleep(RETRY_SLEEP) raise RuntimeError(f"Max retries bereikt voor {url}")def get_all_posts() -> List[Dict[str, Any]]: """Haalt alle posts op van de bron-site met paginatie.""" logger.info("Start ophalen posts van bron-site") posts = [] page = 1 while True: url = f"{SOURCE_BASE_URL}/wp-json/wp/v2/posts" params = { "per_page": 100, "page": page, "status": "publish", } resp = request_with_retry("GET", url, auth=SOURCE_AUTH, params=params) batch = resp.json() if not batch: break posts.extend(batch) logger.info(f"Ophalen posts: pagina {page}, aantal: {len(batch)}") page += 1 time.sleep(SLEEP_BETWEEN_CALLS) logger.info(f"Totaal aantal posts opgehaald: {len(posts)}") return postsdef download_media(media_url: str) -> Optional[bytes]: """Downloadt een mediabestand (bijv. featured image).""" try: resp = request_with_retry("GET", media_url, auth=SOURCE_AUTH) return resp.content except Exception as e: logger.error(f"Fout bij downloaden media {media_url}: {e}") return Nonedef upload_media_to_dest(filename: str, file_bytes: bytes) -> Optional[int]: """Uploadt media naar de doelsite en retourneert attachment ID.""" url = f"{DEST_BASE_URL}/wp-json/wp/v2/media" files = { "file": (filename, file_bytes), } headers = { "Content-Disposition": f'attachment; filename="{filename}"' } try: resp = request_with_retry("POST", url, auth=DEST_AUTH, files=files) data = resp.json() media_id = data.get("id") logger.info(f"Media geüpload naar doelsite, ID: {media_id}") return media_id except Exception as e: logger.error(f"Fout bij uploaden media naar doelsite: {e}") return Nonedef migrate_featured_image(source_post: Dict[str, Any]) -> Optional[int]: """Migreert de featured image van een post en retourneert de nieuwe media ID.""" featured_media_id = source_post.get("featured_media") if not featured_media_id: return None # Haal media-info op media_url = f"{SOURCE_BASE_URL}/wp-json/wp/v2/media/{featured_media_id}" try: resp = request_with_retry("GET", media_url, auth=SOURCE_AUTH) media_data = resp.json() source_media_url = media_data.get("source_url") if not source_media_url: return None file_bytes = download_media(source_media_url) if not file_bytes: return None filename = source_media_url.split("/")[-1] new_media_id = upload_media_to_dest(filename, file_bytes) return new_media_id except Exception as e: logger.error(f"Fout bij migreren featured image voor post {source_post.get('id')}: {e}") return Nonedef create_post_on_dest(source_post: Dict[str, Any], new_featured_id: Optional[int]) -> Optional[int]: """Maakt een nieuwe post aan op de doelsite op basis van de bronpost.""" url = f"{DEST_BASE_URL}/wp-json/wp/v2/posts" title = source_post.get("title", {}).get("rendered", "") content = source_post.get("content", {}).get("rendered", "") excerpt = source_post.get("excerpt", {}).get("rendered", "") date = source_post.get("date") payload = { "title": title, "content": content, "excerpt": excerpt, "status": "publish", "date": date, } if new_featured_id: payload["featured_media"] = new_featured_id try: resp = request_with_retry("POST", url, auth=DEST_AUTH, json=payload) data = resp.json() new_id = data.get("id") logger.info(f"Post aangemaakt op doelsite, nieuwe ID: {new_id}, oude ID: {source_post.get('id')}") return new_id except Exception as e: logger.error(f"Fout bij aanmaken post op doelsite (oude ID {source_post.get('id')}): {e}") return Nonedef migrate_posts(): """Hoofdproces: posts in batches migreren.""" posts = get_all_posts() total = len(posts) logger.info("Start migratie van posts") for i in range(0, total, BATCH_SIZE): batch = posts[i:i + BATCH_SIZE] logger.info(f"Verwerken batch {i}–{i + len(batch) - 1} van {total - 1}") for post in batch: source_id = post.get("id") logger.info(f"Start migratie post ID {source_id}") # 1. Featured image migreren new_featured_id = migrate_featured_image(post) # 2. Post aanmaken op doelsite new_post_id = create_post_on_dest(post, new_featured_id) if new_post_id: logger.info(f"Post succesvol gemigreerd: bron ID {source_id} -> doel ID {new_post_id}") else: logger.error(f"Post NIET gemigreerd: bron ID {source_id}") time.sleep(SLEEP_BETWEEN_CALLS) logger.info(f"Batch {i}–{i + len(batch) - 1} afgerond") # Kleine pauze tussen batches time.sleep(2) logger.info("Migratie voltooid")if __name__ == "__main__": logger.info("=== WordPress migratiescript gestart ===") try: migrate_posts() logger.info("=== WordPress migratiescript succesvol afgerond ===") except Exception as e: logger.exception(f"Onherstelbare fout in migratiescript: {e}") print("Er is een ernstige fout opgetreden, zie logbestand voor details.")
Alternatief zonder database:
WP REST Migrator
WP REST Migrator is een open‑source tool die WordPress‑content migreert via de WordPress REST API, één post tegelijk (minder foutgevoelig). Het is gebouwd om grote, rommelige of zwaar belaste WordPress‑sites te migreren zonder traditionele database‑exports of plugin‑gebaseerde migraties die vaak vastlopen bij grote sites.
🚀 Wat het precies doet
WP REST Migrator (WRM) migreert alle content van een WordPress‑site door elk item via de REST API op te halen en opnieuw in te voeren op een nieuwe installatie. Dit omvat:
- Posts & pages
- Media (inclusief embedded afbeeldingen)
- Tags & categorieën
- Comments
- Featured images
- Metadata
Het script downloadt media van de oude site en uploadt ze opnieuw naar de nieuwe site, zodat WordPress ze correct indexeert en nieuwe ID’s aanmaakt.
💡 Waarom?
De maker ontwikkelde WP REST Migrator omdat traditionele migratie‑plugins vaak falen bij:
- Sites groter dan 5 GB
- Hosts met beperkte PHP‑resources
- phpMyAdmin‑imports die time‑outs veroorzaken
In plaats van een volledige database‑dump te verplaatsen,
migreert WRM alleen de content die je écht nodig hebt,
waardoor je nieuwe WordPress‑installatie schoner en sneller wordt.
🧠 Hoe werkt het?
- Het script haalt via de REST API alle posts op.
- Het scant elke post op media‑URL’s.
- Het downloadt elke afbeelding en uploadt die naar de nieuwe site.
- Het maakt nieuwe posts aan op de doelsite met correcte verwijzingen.
- Het vervangt oude media‑ID’s door nieuwe.
Dit gebeurt post‑voor‑post, waardoor het robuust is tegen time‑outs en serverbeperkingen.
🛠️ Wat maakt WP REST Migrator uniek?
- Post‑voor‑post migratie → minder kans op crashes
- Automatische media‑herindexatie
- Schoont je database op door alleen content over te zetten
- Open‑source (GPLv3)
- Python‑gebaseerd → makkelijk uitbreidbaar
🧭 Wanneer is WP REST Migrator top?
- Je site is groot (>5 GB)
- Je wil alleen content, niet de hele database
- Je wil een schone nieuwe installatie
- Je hebt problemen met traditionele migratie‑plugins
- Je wil volledige controle over het migratieproces
🆚 Alternatieven
| Tool | Werkwijze | Beste voor |
|---|---|---|
| WP REST Migrator | REST API, post‑voor‑post | Grote sites, schone migraties |
| WP Migrate Pro | Database push/pull | Dev‑teams, staging workflows |
| WP Migrate Lite | Database export | Kleine tot middelgrote sites |
| WP‑API Content Migration | REST API via Acorn | Laravel/Roots‑ecosystem |




Geef een reactie