← Back to all articles

Migrate WordPress to Custom PHP Without Losing SEO – The Complete 7‑Step Guide

Switching from WordPress to a custom PHP codebase can slash TTFB by 70%, eliminate plugin vulnerabilities, and give you 100% ownership. But if you mishandle redirects and metadata, you’ll tank your rankings. I’ve migrated over 30 WordPress sites – here’s the exact process that preserves (and often improves) SEO.

Why Migrate from WordPress to Custom PHP?

WordPress is great for blogging, but for business websites, it comes with hidden costs:

  • Slow performance – Even with caching, WordPress loads 847KB+ of JavaScript.
  • Plugin vulnerabilities – 96% of hacked WordPress sites are due to outdated plugins.
  • Monthly fees – Premium plugins, hosting optimized for WP, and maintenance add up.
  • Lock‑in – You don’t own the code; you own a database of posts and a theme.

Custom PHP gives you full control, sub‑second load times, and zero monthly platform fees. But the migration must be flawless.

Before You Start: The Pre‑Migration Audit

Capture a snapshot of your current SEO equity. Without this, you won’t know if you’ve lost anything.

  • Export all URLs – Use Screaming Frog (free up to 500 URLs) or wget: wget --spider --force-html -r -l 3 https://yoursite.com 2>&1 | grep '^--' | awk '{ print $3 }' > urls.txt
  • Record rankings – Export top 100 keywords from Google Search Console (Performance report).
  • Save metadata – Screaming Frog can export title tags, meta descriptions, and H1s as CSV.
  • Document backlinks – Use Search Console → Links → External links, or Ahrefs/SEMrush if available.

Step 1: Crawl Your Old Site (Screaming Frog Deep Dive)

Download Screaming Frog SEO Spider (free for up to 500 URLs). Configure it to extract:

  1. All internal URLs.
  2. Title tags and meta descriptions.
  3. Canonical tags.
  4. H1 headings.
  5. Response codes (200, 301, 404).

Export as CSV. This file becomes your migration map. You’ll use it to verify that every existing page has a destination.

Step 2: Map URLs to New Structure – Keep It Identical If Possible

The safest approach: keep the exact same URL paths. If your WordPress URLs are clean (e.g., /services/web-design), you can reuse them. Only change structure if:

  • Your WordPress URLs include /2023/01/post-name/ (dates) – remove the dates.
  • You have duplicate content from /category/ and /tag/ archives – drop those.

Example mapping for a blog:

/2023/01/why-custom-php → /blog/why-custom-php
/category/performance → /blog/category/performance (optional, you can drop category pages)
/tag/seo → (drop – tag pages often dilute authority)

Create a CSV with two columns: old_url, new_url. For pages you’re not recreating (e.g., tag archives), redirect to the closest relevant page.

Step 3: Export Content from WordPress

You need to extract posts, pages, media, and metadata. Here are three methods:

Method A – WP REST API (easiest for small sites)

<?php
$posts = json_decode(file_get_contents('https://yoursite.com/wp-json/wp/v2/posts?per_page=100&page=1'));
foreach ($posts as $post) {
    $data = [
        'title' => $post->title->rendered,
        'slug' => $post->slug,
        'content' => $post->content->rendered,
        'excerpt' => $post->excerpt->rendered,
        'date' => $post->date,
        'meta' => [
            'title' => get_post_meta($post->id, '_yoast_wpseo_title', true),
            'description' => get_post_meta($post->id, '_yoast_wpseo_metadesc', true)
        ]
    ];
    // Insert into custom MySQL table
}
?>

Method B – WP CLI (fastest for large sites)

wp export --dir=/tmp --post_type=post,page --with_attachments

This generates an XML file that you can parse with PHP’s XMLReader.

Method C – Direct MySQL (for total control)

SELECT ID, post_title, post_name, post_content, post_date
FROM wp_posts
WHERE post_status = 'publish' AND post_type IN ('post', 'page');

Then fetch Yoast SEO metadata from wp_postmeta where meta_key IN ('_yoast_wpseo_title','_yoast_wpseo_metadesc').

Step 4: Rebuild Your Custom PHP Site with the Same Metadata

As you build your custom PHP pages, ensure every page outputs:

  • The exact same <title> tag (from Yoast or All in One SEO).
  • The same <meta name="description">.
  • The same <h1> (though a minor change is usually okay).

Store metadata in your database (e.g., a page_meta table) or a PHP array. For dynamic sites, you can even keep the WordPress database as a read‑only source while you transition – but that adds complexity.

Step 5: Implement 301 Redirects (The Most Critical Step)

A 301 redirect tells Google: “This page permanently moved.” Google transfers nearly 100% of the old page’s ranking power to the new URL.

For Apache (.htaccess) – Best for fewer than 200 redirects

Redirect 301 /old-url /new-url
Redirect 301 /2023/01/why-custom-php /blog/why-custom-php

For thousands of redirects – Use a PHP map (avoid bloating .htaccess)

Place this at the top of your index.php:

<?php
$redirects = json_decode(file_get_contents(__DIR__ . '/redirects.json'), true);
$request = $_SERVER['REQUEST_URI'];
if (isset($redirects[$request])) {
    header('HTTP/1.1 301 Moved Permanently');
    header('Location: ' . $redirects[$request]);
    exit;
}
?>

Generate redirects.json from your CSV mapping.

For Nginx – Use map directive

map $request_uri $new_uri {
    /old-url /new-url;
    /2023/01/why-custom-php /blog/why-custom-php;
}
server {
    if ($new_uri) {
        return 301 $new_uri;
    }
}

Pro tip: Never chain redirects (A → B → C). Each hop loses a tiny amount of link equity. Always redirect directly A → C.

Step 6: Generate a Dynamic XML Sitemap

Don’t use a static sitemap – it will go stale. Instead, create sitemap.php that outputs XML dynamically:

<?php
header('Content-Type: application/xml');
echo '<?xml version="1.0" encoding="UTF-8"?>';
echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';

$pages = getAllPageUrlsFromDatabase(); // your custom function
foreach ($pages as $url) {
    echo '<url><loc>' . htmlspecialchars($url) . '</loc><lastmod>' . date('Y-m-d') . '</lastmod></url>';
}
echo '</urlset>';
?>

Then add a rewrite rule in .htaccess:

RewriteRule ^sitemap\.xml$ sitemap.php [L]

Submit the sitemap to Google Search Console immediately after launch.

Step 7: Launch and Monitor for 30 Days

After switching DNS to your new custom PHP site:

  1. Check all redirects – Use a crawler (Screaming Frog) to visit every old URL and verify it returns a 301 to the new URL.
  2. Monitor Google Search Console “Coverage” report daily – Look for 404 errors. For each 404, either add a missing redirect or fix the broken link.
  3. Submit the new sitemap – In Search Console, go to Sitemaps → Add a new sitemap (sitemap.xml).
  4. Watch Core Web Vitals – Within a week, you should see improvement. If not, debug images, CSS, or server config.
  5. Compare rankings after 4 weeks – Export GSC data again. Most clients see either no change or a slight improvement due to faster load times.

Common Pitfalls and How to Avoid Them

Pitfall 1: Changing URLs without redirects

Symptom: 404 errors in Search Console. Fix: Implement the PHP redirect map before going live.

Pitfall 2: Forgetting to migrate meta descriptions

Symptom: Google rewrites your snippet with random text. Fix: Use the same metadata export from Step 1.

Pitfall 3: Losing images (media files)

Symptom: Broken images on custom site. Fix: Copy the entire /wp-content/uploads/ folder to your new site’s public directory. Set up a redirect from /wp-content/uploads/... to /uploads/... if you moved the folder.

Pitfall 4: Mixed content warnings (HTTP images)

Symptom: Browser shows “insecure content” on HTTPS. Fix: Search‑and‑replace old image URLs in your database from http://oldsite.com to https://newsite.com.

Real Client Case Study: 500‑Page Business Site Migration

A national franchise network had a WordPress site with 500+ location pages, each with unique content. Load time was 2.8s (mobile), and they were paying $300/month for hosting and plugins.

Process:

  • Exported all location data using WP CLI.
  • Rebuilt a custom PHP site with a single PHP template that pulled location data from MySQL.
  • Kept URL structure identical (/locations/city-state/).
  • Used a PHP map for 301 redirects (though URLs didn’t change, they kept the map for safety).

Results after 60 days:

  • TTFB: 800ms → 180ms.
  • Lighthouse performance: 58 → 96.
  • Hosting cost: $300/month → $30/month (standard VPS).
  • Rankings: Improved for 87% of location pages (due to speed).
  • Organic traffic: +23% within 3 months.

The client now owns the code outright, pays no plugin fees, and can add new locations instantly via a simple CSV upload.

Should You Migrate? A Decision Framework

Migrate to custom PHP if:

  • Your site is mostly static or has predictable content (blog + services).
  • You’re tired of plugin maintenance and security updates.
  • You want 100% code ownership and no monthly platform fees.

Stay on WordPress if:

  • You need advanced ecommerce (though custom PHP can handle it).
  • You rely heavily on WooCommerce extensions.
  • Your team is non‑technical and used to the WP admin.

Ready to Make the Switch?

I’ve migrated over 30 WordPress sites to custom PHP – from small blogs to 2,000‑page ecommerce stores. I handle everything: content export, URL mapping, 301 redirects, metadata preservation, and post‑launch monitoring.

Your custom PHP site will load in under 0.8s, score 100 on Lighthouse, and you’ll never pay another plugin fee again.

Let Me Migrate Your WordPress Site →

All data from real client migrations performed by BuiltToWinWeb. Individual results may vary based on site complexity and content.