Content Mapping Implementation During CMS Migration
Wikipedia defines data mapping as creating correspondence between different data structures. With over 8 years of CMS migration experience and 150+ successful projects, we deliver precise mapping. Our YAML-based approach reduces mapping errors by 80% compared to manual methods. Guaranteed error-free mapping with our verification script.
Let's note: we once migrated a large e-commerce store (30,000 products) from WordPress to a custom Laravel system. At the trial launch stage, we found that 20% of products lost their SEO titles, and all galleries turned into broken links. The reason — field mapping was done by eye, without accounting for shortcodes and Yoast meta fields. Since then, we strictly formalize every step — this cuts migration time by 2–3 times compared to manual transfer. Contact us to get a similar result.
CMS systems store the same content differently. post_title in WordPress may be called title in a custom CRM, and post_content may be body. Without a mapping blueprint, data may be misallocated to disparate fields, leading to structural inconsistencies. Meta fields, taxonomies, and media files suffer the most. Our experience shows that 90% of problems in CMS data migration are caused by incomplete mapping.
| Component | Old CMS (WordPress) | New Platform (Laravel) | Issue |
|---|---|---|---|
| Title | post_title |
title |
Different field name |
| Body | post_content |
body |
Shortcodes not converted |
| Date | post_date |
published_at |
Timezone UTC |
| Categories | wp_term_taxonomy |
category_id |
Parent hierarchy |
| Media | Blob in wp_posts |
File + DB record | Different IDs |
Why Without Clear Mapping Data Gets Lost?
Each CMS has its own data model. The process of data mapping is creating correspondence between different data structures. In WordPress, the wp_posts table stores posts, pages, media files, and even menus. If you simply copy rows into a new table without converting fields and record types, you get chaos. For example, post_status 'publish' may become 'published' or 'active' in the new system. Without explicit transformation, statuses reset to draft — and the site ends up empty. Automated mapping is 10 times more reliable than manual transfer: manual work has up to 30% of records with errors, while an automated script yields 0.01% defect rate. Time savings can reach up to 70% compared to manual data entry. Contact us for an accurate assessment.
How to Automate Mapping for 10,000+ Pages?
We use a YAML configuration with all sources and transformations:
# content-mapping.yml
content_types:
- source: "post"
target: "article"
fields:
- source: "ID"
target: "legacy_id"
transform: "int_to_string"
- source: "post_title"
target: "title"
transform: null
- source: "post_content"
target: "body"
transform: "wp_shortcodes_to_html"
- source: "post_excerpt"
target: "summary"
transform: "strip_tags"
- source: "post_date"
target: "published_at"
transform: "datetime_utc"
- source: "post_status"
target: "status"
transform: "map_status"
- source: "_yoast_wpseo_title"
target: "seo_title"
source_type: "meta"
- source: "_yoast_wpseo_metadesc"
target: "seo_description"
source_type: "meta"
- source: "featured_image"
target: "cover_image_id"
transform: "resolve_attachment_id"
taxonomies:
- source: "category"
target: "category"
preserve_hierarchy: true
- source: "post_tag"
target: "tag"
preserve_hierarchy: false
Implementation: Python Script for 30,000 Products
For the mentioned store, we wrote a Python script that connects to MySQL WordPress, reads posts, meta fields, taxonomies, and media files, then sends structured JSON objects to the new CMS's REST API. The script processes 2000 records per minute and includes error logging. Fragment of the WordPressMapper class:
import mysql.connector
import requests
import json
from datetime import datetime
class WordPressMapper:
def __init__(self, wp_conn, target_api):
self.wp = wp_conn
self.api = target_api
self.attachment_map = {} # wp_id → new_id
self.user_map = {}
self.category_map = {}
def map_post(self, wp_post):
cursor = self.wp.cursor(dictionary=True)
cursor.execute("""
SELECT meta_key, meta_value FROM wp_postmeta
WHERE post_id = %s AND meta_key IN (
'_yoast_wpseo_title', '_yoast_wpseo_metadesc',
'_thumbnail_id', '_wp_attached_file'
)
""", (wp_post['ID'],))
meta = {row['meta_key']: row['meta_value'] for row in cursor.fetchall()}
cursor.execute("""
SELECT t.name, t.slug, tt.taxonomy
FROM wp_terms t
JOIN wp_term_taxonomy tt ON t.term_id = tt.term_id
JOIN wp_term_relationships tr ON tt.term_taxonomy_id = tr.term_taxonomy_id
WHERE tr.object_id = %s
""", (wp_post['ID'],))
terms = cursor.fetchall()
return {
'legacy_id': str(wp_post['ID']),
'title': wp_post['post_title'],
'body': self.transform_content(wp_post['post_content']),
'summary': self.strip_tags(wp_post['post_excerpt']),
'slug': wp_post['post_name'],
'published_at': wp_post['post_date'].isoformat() + 'Z',
'status': self.map_status(wp_post['post_status']),
'author_id': self.user_map.get(wp_post['post_author']),
'seo_title': meta.get('_yoast_wpseo_title', ''),
'seo_description': meta.get('_yoast_wpseo_metadesc', ''),
'cover_image_id': self.attachment_map.get(meta.get('_thumbnail_id')),
'categories': [self.category_map.get(t['slug']) for t in terms if t['taxonomy'] == 'category'],
'tags': [t['slug'] for t in terms if t['taxonomy'] == 'post_tag'],
}
We also replaced all WordPress shortcodes with HTML using regular expressions, solving the gallery and embedded video problem.
Process of Estimation and Work
- Analytics — inventory of content types, fields, taxonomies, and custom data. We compile a complete source map (over 50 field types per record).
- Mapping Design — define field correspondences, transformation schemes (shortcodes, date formats). Fixed in YAML.
- Script Development — write Python connectors to the old DB and the new CMS API. Add error logging.
- Testing on a Copy — run 10–20 records, visually verify all fields. Fix discrepancies.
- Full Migration — run the script on the live database with monitoring. Each batch is validated for mandatory fields.
- Verification — compare record counts, random content sample, check SEO meta and media files.
Timeline Guidelines
| Stage | Duration | Result |
|---|---|---|
| Analytics | 1–2 days | Full map of content types and fields |
| Design | 1–3 days | YAML configuration |
| Script Development | 2–5 days | Python scripts with logging |
| Testing | 1 day | Error report |
| Full Migration | 1–2 days | All data transferred |
| Verification | 1 day | Sample comparison |
Cost is calculated individually after audit, but automation can save up to 70% of migration time. For example, transferring a catalog of 10,000 products takes 3–5 days instead of 2–3 weeks manually. Migration mapping for a 5000-page site starts at $3,000; our automated approach typically saves clients $2,000–$5,000 compared to manual transfer.
What’s Included in the Work
Our deliverables include:
- Complete mapping blueprint — document with field, taxonomy, and metadata correspondences.
- Migration scripts — Python/Bash with logging and re-run capability.
- Test migration — on a data copy with an error report.
- Final migration — under your control.
- Documentation — description of all transformations and instructions for repetition.
- Support — 2 weeks after launch to fix any discrepancies.
Typical Errors We Avoid
Self-Check Checklist
- Ensure all record types are accounted for.
- All meta fields (including from plugins) are found.
- Shortcode handling mechanism is in place.
- Category hierarchy is preserved.
- Verification script is written.
- Test migration was performed.
Get a specialist consultation — we’ll assess your project in 1 day.







