Migrate Legacy PHP to Craft CMS: Complete Step-by-Step Guide

Migrate Legacy PHP to Craft CMS: Complete Step-by-Step Guide

Alex Rollin
Alex Rollin
January 23, 2026
Last updated : February 15, 2026
January 23, 2026

That aging PHP website has served you well. Maybe it's running on PHP 5.2, with content scattered across flat files, custom database tables, and templates that mix logic with presentation in ways that make you wince. The site still works, but every update feels like defusing a bomb.

Craft CMS offers a path forward. Built on PHP 8.2 , Craft brings flexible content modeling, clean Twig templating, and strong import tools that make migration manageable. This Craft CMS migration guide walks you through the complete process, from auditing your legacy content to importing it into a fresh Craft installation. Whether you're handling a legacy PHP website migration for the first time or planning a larger modernization effort, this guide covers every step.

Prerequisites

Before starting your migration, ensure you have the following in place:

Development Environment

  • Local development setup with PHP 8.2 (DDEV works well for this)
  • Composer 2.0 installed
  • MySQL 8.0.17 or PostgreSQL 13 
  • At least 256MB PHP memory (512MB recommended)

Access Requirements

  • Read access to your legacy database
  • Access to your legacy file uploads directory
  • FTP or SSH access to your current hosting

Knowledge Requirements

  • Basic PHP understanding (you have this if you're maintaining a legacy PHP site)
  • Familiarity with SQL queries for data extraction
  • Willingness to learn Twig basics (similar logic patterns to PHP, cleaner syntax)

Important note: Your PHP 5.2 hosting environment cannot run Craft. Plan to deploy Craft on new infrastructure that meets these requirements while keeping your legacy site running during the migration.

Step 1: Audit Your Legacy Content

Start by cataloging everything your old site contains. This inventory shapes your entire migration approach.

Identify content types:

  • Blog posts, articles, or news items
  • Static pages (About, Contact, Services)
  • Product listings or portfolio items
  • User-generated content

Map relationships:

  • Categories or tags attached to content
  • Related items or cross-references
  • Author associations

Inventory media:

  • Where files live (usually an uploads folder)
  • File naming patterns
  • Whether filenames should be preserved or normalized

Document URL patterns:

  • Current URL structure for each content type
  • Which URLs receive traffic (check analytics)
  • Canonical URLs you need to preserve for SEO

Create a spreadsheet tracking each content type, its database table (if applicable), field count, and record count. This becomes your migration checklist.

Step 2: Design Your Craft Content Model

This step determines whether your migration succeeds or becomes a frustrating mess. We've learned that spending extra time here saves significant rework later. Proper Craft CMS content modeling is essential for a successful migration.

Sections vs. Entry Types:

In Craft, a Section is a container for related content. Entry Types define the field layouts within a Section. Understanding Craft CMS sections and entry types is fundamental to this process.

For example:

  • A "Blog" Section might have a single Entry Type
  • A "News" Section might have Entry Types for "Press Release" and "Company Update" with different fields

Field planning:

Map your legacy data fields to Craft field types:

  • Text columns → Plain Text or Rich Text fields
  • HTML content → Redactor or CKEditor fields
  • Dates → Date fields
  • Foreign keys → Entries relations fields
  • Image paths → Assets fields

URL structure decisions:

Craft lets you define URL patterns per Section. Decide now whether to:

  • Match your legacy URL structure exactly
  • Adopt a cleaner URL pattern and redirect old URLs

Create your Sections, Entry Types, and Fields in Craft before importing any content. Test by manually creating one or two entries to verify your model captures all necessary data.

Step 3: Export Your Legacy Data

You need to get your data out of the old system in a format Craft can consume. Feed Me, Craft's import plugin, accepts JSON, CSV, and XML. A legacy database export to JSON format works well for most scenarios.

For database-driven content:

Create a PHP script that queries your legacy database and outputs JSON:

// legacy-export.php
$pdo = new PDO('mysql:host=localhost;dbname=legacy_db', 'user', 'pass');

$articles = $pdo->query("
    SELECT id, title, body, author, created_at, category_id 
    FROM articles 
    ORDER BY created_at
")->fetchAll(PDO::FETCH_ASSOC);

// Clean and transform data
foreach ($articles as &$article) {
    // Convert legacy date format
    $article['postDate'] = date('Y-m-d H:i:s', strtotime($article['created_at']));
    
    // Strip problematic HTML if needed
    $article['body'] = strip_tags($article['body'], '<p><a><strong><em><ul><li>');
}

header('Content-Type: application/json');
echo json_encode($articles, JSON_PRETTY_PRINT);

For file-based content:

If your legacy site stores content in PHP files, you'll need to parse them:

// For sites with content in PHP arrays or includes
$content = [];
foreach (glob('pages/*.php') as $file) {
    include $file;
    $content[] = [
        'title' => $page_title,
        'body' => $page_content,
        'slug' => basename($file, '.php')
    ];
}

echo json_encode($content, JSON_PRETTY_PRINT);

Save your export files or host them at accessible URLs for Feed Me to consume.

Step 4: Set Up Feed Me and Configure Your Import

The Feed Me plugin for Craft CMS is the recommended tool for content imports. Install Feed Me from the Craft Plugin Store (it's free and officially maintained):

composer require craftcms/feed-me
php craft plugin/install feed-me

Create a new feed in the Control Panel under Feed Me → Feeds → New Feed:

  • Feed URL: Point to your JSON export file or URL
  • Feed Type: Select JSON (or CSV/XML based on your export)
  • Primary Element: Usually the root array in your JSON
  • Element Type: Entries (for most content migrations)
  • Target Section: Select the Section you created in Step 2

Field mapping:

Feed Me presents a visual mapping interface. For each Craft field, select the corresponding key from your legacy data:

Craft Field          Legacy JSON Key
-----------          ---------------
Title                title
Post Date            postDate
Body (Rich Text)     body
Author               author

Handling relationships:

If your legacy content has categories or tags, you have two options:

  • Import categories first as a separate feed, then map article categories by matching on name or legacy ID
  • Let Feed Me create categories on the fly during article import (simpler but less control)

Duplicate handling:

Feed Me can update existing entries rather than creating duplicates. Set your unique identifier (usually legacy ID or slug) in the Duplication Handling section.

Step 5: Migrate Your Assets

Media files require separate handling. Craft CMS asset migration depends on your hosting setup.

Option A: Manual transfer

  • Download your legacy uploads folder
  • Create an Asset Volume in Craft pointing to web/uploads (or similar)
  • Upload files via FTP or rsync to your new server
  • During content import, map asset filenames to the corresponding Craft Assets

Option B: Feed Me asset import

Feed Me can import assets from URLs. Include full URLs to images in your JSON export:

{
  "title": "Article Title",
  "featuredImage": "https://legacy-site.com/uploads/image.jpg"
}

Feed Me will download and create Asset entries automatically when you map the field.

Option C: Custom migration script

For large media libraries with complex organization, a custom console command gives you full control:

// modules/console/controllers/MigrateController.php
namespace modules\console\controllers;

use craft\console\Controller;
use craft\elements\Asset;
use craft\helpers\Assets as AssetsHelper;

class MigrateController extends Controller
{
    public function actionAssets()
    {
        $legacyFiles = glob('/path/to/legacy/uploads/*');
        $volume = \Craft::$app->volumes->getVolumeByHandle('uploads');
        
        foreach ($legacyFiles as $file) {
            $asset = new Asset();
            $asset->tempFilePath = $file;
            $asset->filename = basename($file);
            $asset->volumeId = $volume->id;
            
            \Craft::$app->elements->saveElement($asset);
            $this->stdout("Imported: " . basename($file) . "\n");
        }
    }
}

Step 6: Convert Your Templates

Your legacy PHP templates need to become Twig templates. PHP to Twig template conversion follows similar logic patterns, but the syntax is cleaner.

Basic output:

// Legacy PHP
 echo htmlspecialchars($article['title']); ?>

// Craft Twig
{{ entry.title }}

Note that Twig auto-escapes output by default, so you don't need htmlspecialchars calls.

Loops:

// Legacy PHP
 foreach ($articles as $article): ?>
    <h2> echo $article['title']; ?></h2>
 endforeach; ?>

// Craft Twig
{% for entry in craft.entries.section('articles').all() %}
    <h2>{{ entry.title }}</h2>
{% endfor %}

Conditionals:

// Legacy PHP
 if (!empty($article['image'])): ?>
    <img src=" echo $article['image']; ?>">
 endif; ?>

// Craft Twig
{% if entry.featuredImage|length %}
    <img src="{{ entry.featuredImage.one().url }}">
{% endif %}

Including partials:

// Legacy PHP
 include 'header.php'; ?>

// Craft Twig
{% include '_partials/header' %}

Work through your templates page by page. Craft's template structure typically looks like:

templates/
  _layouts/
    base.twig
  _partials/
    header.twig
    footer.twig
  articles/
    _entry.twig (single article)
    index.twig (article listing)
  index.twig (homepage)

Step 7: Set Up Redirects

Preserving your URL structure protects SEO value and prevents broken links. Proper URL redirects for SEO migration are critical. Craft offers multiple redirect approaches.

For simple redirects:

The Retour plugin (by nystudio107) provides a Control Panel interface for managing redirects with wildcard support and 404 tracking. The Retour plugin for redirects is the most popular solution in the Craft ecosystem.

For pattern-based redirects:

If your URL structure changed predictably, use Craft's routes or server-level redirects:

# nginx example
location ~ ^/articles/(\d )$ {
    return 301 /blog/$1;
}

For complex redirects:

Create a redirect map during your content export that pairs old URLs with new ones:

// During export, generate redirect pairs
$redirects = [];
foreach ($articles as $article) {
    $redirects[] = [
        'old' => '/articles/' . $article['id'],
        'new' => '/blog/' . $article['slug']
    ];
}
file_put_contents('redirects.json', json_encode($redirects));

Import this map into Retour or your server configuration.

Common Mistakes to Avoid

Our experience shows that certain pitfalls trip up almost every migration project:

Skipping the content audit. Jumping straight to importing leads to discovering missing fields mid-migration. The audit prevents this.

Ignoring character encoding. Legacy databases often use latin1 encoding. Craft expects UTF-8. Run your data through encoding conversion during export:

$article['body'] = mb_convert_encoding($article['body'], 'UTF-8', 'ISO-8859-1');

Not cleaning legacy HTML. Old WYSIWYG editors produced messy markup. Clean it before import, not after:

// Remove inline styles, empty tags, proprietary tags
$article['body'] = preg_replace('/style="[^"]*"/', '', $article['body']);

Importing without testing first. Run Feed Me imports on a subset of content first (add a LIMIT clause to your export query). Verify the results before importing everything.

Forgetting about drafts and unpublished content. Decide upfront whether to migrate drafts. If not, filter them out during export.

Underestimating media organization. A legacy uploads folder with 10,000 files and no structure becomes a problem in Craft. Consider organizing into subfolders during migration.

Testing and Verification

Before launching your migrated site, verify the migration thoroughly.

Content verification:

  • Compare record counts between legacy and Craft
  • Spot-check 10-20 entries across different content types
  • Verify that rich text content renders correctly
  • Check that images display and link properly

Relationship verification:

  • Confirm categories and tags attached correctly
  • Test any related entries connections
  • Verify author assignments

URL verification:

  • Test redirects from old URLs to new ones
  • Check that no 404s appear for important pages
  • Run a crawler (Screaming Frog or similar) to find broken internal links

Template verification:

  • Compare rendered pages visually with the legacy site
  • Test responsive behavior if templates changed
  • Check forms, search, and any interactive features

Performance verification:

  • Run a performance test (Lighthouse, WebPageTest)
  • Check that caching works as expected
  • Verify that asset transforms generate correctly

Create a checklist of critical pages and features. Have someone unfamiliar with the migration test the site fresh.

Conclusion

Migrating a legacy PHP site to Craft CMS involves careful planning, methodical data extraction, and patient field mapping. When you migrate PHP to Craft CMS, the Feed Me plugin handles most import scenarios well, though complex migrations may benefit from custom console commands that give you full control over the process.

The effort pays off with a content management system that your team will actually enjoy using, built on current PHP standards with years of support ahead.

We recommend approaching these migrations in phases, running test imports early and often. Getting the content model right before importing saves significant rework.

If you're planning a migration from an aging PHP site to Craft CMS, we can help you assess the scope and build a migration plan that accounts for your specific data structures and URL requirements. Our website migration services cover everything from initial audit to final launch. Reach out to discuss your project.

Share this article