Performance11 min read

How to Migrate Images to a CDN Without Losing SEO

A complete image CDN migration checklist: inventory every image URL, choose between identical paths and a new hostname, set up 301 redirects, warm the cache, and monitor Search Console so rankings on image search survive the move.

By ImageGuide Team·Published August 21, 2026·Updated August 21, 2026
image cdn migrationchange image urls without losing seomigrate images to cdn301 redirectcdnperformanceseo

Moving images to a CDN is one of the highest-value performance changes you can make. It is also one of the easiest ways to destroy years of accumulated image search traffic. An image CDN migration touches every page on your site at once, and the failure modes are quiet: rankings slip over weeks, 404s pile up in logs nobody reads, and old URLs sit in caches serving stale copies to half the world.

The good news: a careful migration almost never loses rankings. The difference between a careful migration and a careless one is process, not budget. This guide gives you that process — an exact inventory, a URL strategy decision, a redirect plan, cache warmup, sitemap resubmission, monitoring, a rollback plan, and a day-by-day timeline from seven days before launch to thirty days after.

Why Image Migrations Go Wrong

Before the checklist, understand the three ways migrations fail. Every horror story reduces to one of these.

Google Images indexes images by URL. When the URL changes, Google must recrawl, re-evaluate, and re-rank the image at its new address. During that window — typically a few days to a few weeks — images rank poorly or not at all. If you change URLs without redirects, Google treats the old images as deleted and the new ones as brand new content with no history. The old URLs eventually drop out of the index, and the new ones start from zero.

This matters more than people expect. For e-commerce, recipe, travel, and design sites, image search drives a meaningful share of total organic traffic. A site that loses its image rankings loses that channel for weeks or months.

2. The 404 storm

Old URLs do not disappear when you deploy. They live in:

  • Cached HTML pages (CDN edge caches, browser caches, Google’s own cache)
  • Sitemaps you forgot to regenerate
  • Social media posts, messaging apps, and forums that embed images directly
  • Third-party sites that hotlinked your images
  • Email campaigns and PDFs that reference absolute image URLs

Every one of those references keeps requesting the old URL. If the old URL now returns 404, users see broken images and crawlers record errors. A migration that skips redirects turns every historical reference into a permanent broken image.

3. Double-cached old URLs

The nastiest failure. Your origin updates, your CDN caches the new HTML, but the old image URLs remain cached at the edge — or worse, cached with long TTLs from your old cache headers. Users get fresh HTML pointing at new image URLs, but some intermediate cache still serves the old HTML pointing at old URLs that now 404. You get intermittent breakage that you cannot reproduce, because every time you test, your own cache is warm and correct.

The fix is to treat cache invalidation as part of the migration plan, not an afterthought. Purge old URLs deliberately, and set sane cache lifetimes on the new host from day one.

Step 1: Inventory Every Image Before You Touch Anything

Never migrate from memory. You need a complete list of every image URL your site serves and every page that references it. Three ways to get there, from easiest to most thorough.

Option A: Crawl your own site with wget

For a small to medium site, one command produces a full inventory:

wget --spider --recursive --level=5 --no-parent \
  --domain=example.com \
  https://example.com/ 2>&1 | grep -oE 'https?://[^ ]+\.(png|jpe?g|webp|avif|gif|svg)' \
  | sort -u > image-urls.txt

This walks your site, extracts every image URL it encounters, and deduplicates them. It follows links but does not parse CSS background-image references or lazy-loaded markup that wget does not execute — so treat it as a floor, not a ceiling.

Option B: Screaming Frog

Screaming Frog’s SEO Spider has a dedicated Images tab. Crawl your site, open the Images report, and you get every image URL, its alt text, its size, its response code, and — critically — the Inlinks column showing which pages reference it. Export the whole thing to a spreadsheet. This is the inventory you want: URL plus referencing pages plus current status.

Filter for images that return non-200 responses while you are there. Migrating is the perfect moment to discover you have been serving 404s for months.

Option C: A Node crawler script

For large or dynamic sites, script the crawl yourself:

import { parse } from 'node-html-parser';

const BASE = 'https://example.com';
const seenPages = new Set();
const imageRefs = new Map(); // imageUrl -> Set of pages

async function crawl(url) {
  if (seenPages.has(url)) return;
  seenPages.add(url);
  const res = await fetch(url);
  if (!res.ok) return;
  const html = await res.text();
  const root = parse(html);

  for (const img of root.querySelectorAll('img')) {
    const src = img.getAttribute('src') || img.getAttribute('data-src');
    if (!src) continue;
    const abs = new URL(src, url).href;
    if (!imageRefs.has(abs)) imageRefs.set(abs, new Set());
    imageRefs.get(abs).add(url);
  }

  const links = root.querySelectorAll('a[href]')
    .map(a => new URL(a.getAttribute('href'), url).href)
    .filter(href => href.startsWith(BASE) && !seenPages.has(href));

  // Limit concurrency in real use; sequential shown for clarity
  for (const link of links.slice(0, 20)) await crawl(link);
}

await crawl(BASE + '/');
for (const [img, pages] of [...imageRefs].sort()) {
  console.log(img, '<-', [...pages].join(', '));
}

The output — image URL mapped to referencing pages — is the artifact that drives every later step. Save it. When something breaks at T+2, you will look up the broken URL here and know exactly which pages to check.

Your inventory should also capture, per image URL: current content type, file size, and whether it is referenced in CSS, JavaScript, or structured data — not just <img> tags. CSS and JSON-LD references are the ones teams forget.

Step 2: Choose Your URL Strategy

This is the decision that determines how painful the migration is. There are two strategies, and they have very different risk profiles.

Strategy A: Keep identical paths behind a new CDN host (lowest risk)

You serve the same paths as today, but from a custom domain or CNAME pointed at your CDN. Example: your images stay at https://cdn.example.com/images/hero.jpg and that hostname now routes through your CDN instead of your origin — or, even better, your images stay on https://example.com/images/hero.jpg entirely, and the CDN sits in front of your whole domain.

The zero-cost version: point a subdomain like img.example.com at the CDN via CNAME, and have the CDN pull from your origin at the same path. URLs on your pages can stay exactly as they are if the CDN is transparent (all traffic through one hostname), or change only in the hostname part if you adopt the subdomain.

Why this wins:

  • No path changes means no redirect chains. If the full URL is byte-identical, there is nothing for Google to re-learn.
  • No database rewrites if you keep the same hostnames in your HTML.
  • Rollback is a DNS change. Point the CNAME back and you are done.

The cost: you need control of DNS and the ability to issue a TLS certificate for the custom domain. Most CDNs handle both automatically. If your CDN vendor supports a custom domain (nearly all do), this is almost always the right answer.

Strategy B: New hostname (higher risk, sometimes necessary)

You move images to a new hostname entirely — https://assets.newcdn.io/images/hero.jpg. This happens when the CDN requires its own domain, or you are consolidating multiple origins.

Now every image URL changes, and you must do three extra things:

  1. 301-redirect every old image URL to its new URL (next section).
  2. Verify BOTH hosts in Search Console. The old host’s property shows you the old URLs’ performance and lets you monitor the redirect transition. The new host’s property is where the new URLs will accumulate performance data. Without verifying both, you are flying blind on exactly the metric that matters.
  3. Update every reference in HTML, CSS, JS, structured data, and your sitemap.

This mismatch — new hostnames that Google must associate with your site — is one of the classic image SEO mistakes: teams change image hosts, skip the Search Console verification and redirects, and wonder why image traffic flatlines. The CDN host section of that guide covers the same trap from the setup side; the principle here is identical: a hostname change is a site move for images, and it deserves site-move-level care.

Decision rule: use Strategy A unless a hard constraint forces Strategy B. Identical paths eliminate the entire class of redirect, reindex, and rewrite problems.

Step 3: Set Up 301 Redirects for Old Image URLs

Do image 301s actually matter for Google Images? Yes. Google has confirmed that redirects pass signals for images just as they do for pages. When the old image URL 301s to the new one, Google consolidates the old URL’s indexing history, alt-text associations, and ranking signals onto the new URL. Without the redirect, those signals evaporate and the new URL starts cold.

Set up the redirects even under Strategy A if you changed hostnames at all, and definitely under Strategy B:

  • At the CDN edge: most CDNs let you define redirect rules. Map the old prefix to the new one: anything matching example.com/images/* redirects 301 to cdn.example.com/images/* with the path preserved. Path-preserving rules mean one rule covers your whole inventory.
  • At the origin: if the old host still points at your server, add redirects in nginx, Apache, or your framework before the static file handler:
# nginx: preserve path, redirect old image host to CDN
server {
    server_name example.com;
    location ~* ^/images/.+\.(png|jpe?g|webp|avif|gif|svg)$ {
        return 301 https://cdn.example.com$request_uri;
    }
}

Redirect rules to live by:

  • Always 301, never 302. Temporary redirects do not consolidate signals. Google keeps checking the old URL forever.
  • Preserve the path exactly. request_uri in nginx, equivalent variables elsewhere. One-to-one mapping, no cleverness.
  • Redirect to images, not to the homepage. A redirect to / is a soft 404. It fails the user, fails the crawler, and fails the signal transfer.
  • Keep redirects alive for at least a year. Social posts, forum threads, and cached pages keep hitting old URLs long after your own site has moved on.

Test the redirects with curl -I against a sample of 50 URLs from your inventory before launch. Every one should return 301 with the correct Location header.

Step 4: Database and CMS Find-and-Replace

If you chose Strategy B, your content now references old URLs and must be rewritten. Doing this by hand does not scale; doing it carelessly corrupts data. Here is the safe pattern.

WordPress: the serialized-data trap

The naive approach is direct SQL against wp_posts:

-- DANGEROUS on WordPress: breaks serialized PHP data
UPDATE wp_posts
SET post_content = REPLACE(post_content, 'https://example.com/images/', 'https://cdn.example.com/images/');

This works for plain HTML in post_content, but WordPress stores serialized PHP in postmeta, options, and widget data. Serialized strings embed byte lengths: s:45:"https://example.com/images/hero.jpg";. If your replacement changes the string length and you do not update the length prefix, PHP fails to unserialize the value and silently discards the data. You will corrupt theme options and widget settings without any error message.

The safe options, in order of preference:

  1. WP-CLI’s search-replace command, which understands serialization:
wp search-replace 'https://example.com/images/' 'https://cdn.example.com/images/' --all-tables --dry-run

Run it with --dry-run first, review the counts, then drop the flag. Add --report-changed-modified to keep a record. WP-CLI recalculates serialized lengths correctly.

  1. A migration plugin (for example, the better-search-replace style tools) that does the same serialization-safe replacement through the admin UI, table by table.

  2. Direct SQL only for tables and columns you have verified contain no serialized data — typically post_content alone. Even then, take a database backup first. Every time.

Headless CMS and static sites

For content stored as Markdown, MDX, or JSON, the rewrite is a plain string replacement — but do it in content, not at render time. A runtime filter that rewrites URLs on output hides the old URLs from your own team and makes every future debugging session harder. Rewrite the source content in one commit, review the diff, and deploy:

grep -rl 'example.com/images/' content/ | xargs sed -i 's|example.com/images/|cdn.example.com/images/|g'

Then re-run your inventory crawl from Step 1 against a staging build. The new crawl should show zero references to the old host in HTML. Any stragglers are in templates, CSS, or JavaScript — fix those too. CSS url() references and inline styles are the most common survivors.

Step 5: Warm the Cache Before Launch

A fresh CDN is a cold CDN. The first user to request each image triggers a fetch to your origin, which is slower than either your old setup or the warmed CDN will ever be. On a launch day traffic spike, a cold cache turns your CDN into a reverse-proxy amplifying load on your origin — the opposite of what you installed it for.

Warm the cache before real users arrive:

  1. Feed your inventory URLs to a prefetcher. A simple parallel fetch over the inventory list works:
// warm-cache.mjs — run from a machine with good bandwidth
import { readFile } from 'node:fs/promises';

const BASE = 'https://cdn.example.com';
const urls = (await readFile('image-urls.txt', 'utf8'))
  .split('\n').map(l => l.trim())
  .map(l => l.replace('https://example.com', BASE))
  .filter(Boolean);

const CONCURRENCY = 8;
let i = 0, ok = 0, fail = 0;

async function worker() {
  while (i < urls.length) {
    const url = urls[i++];
    try {
      const res = await fetch(url, { method: 'GET' });
      if (res.ok) ok++; else { fail++; console.error(res.status, url); }
    } catch { fail++; console.error('ERR', url); }
  }
}

await Promise.all(Array.from({ length: CONCURRENCY }, worker));
console.log(`Warmed ${ok} URLs, ${fail} failures`);
  1. Check the response headers, not just the status. Each warmed URL should return a cache HIT on the second request and carry the cache-control headers you intend to ship. If your CDN serves MISS forever, the warmup is not sticking — check whether the CDN respects your origin’s cache headers or needs cache rules configured at the edge.

  2. Warm in popularity order if your inventory has analytics. Your hero images, category headers, and top product photos matter far more than page-40 thumbnails. Warm the head of the distribution first so the highest-traffic images are never cold.

  3. Repeat after every purge. Cache warmup is not one-time. If you purge the CDN at T+1 to fix a bad header, warm again.

For a deeper look at the header side of this — what Cache-Control values to set and how s-maxage and stale-while-revalidate interact with a CDN — see the image caching headers guide. Getting those headers right before launch is what makes warmup stick.

Step 6: Resubmit Sitemaps Pointing at the New Host

Google discovers images largely through pages and image sitemaps. After a hostname change, your sitemap is both a discovery accelerant for the new URLs and a signal that the move is intentional.

  1. Regenerate the sitemap so every image entry points at the new host. An image sitemap uses the image:image extension:
<url>
  <loc>https://example.com/products/steel-mug</loc>
  <image:image>
    <image:loc>https://cdn.example.com/images/steel-mug.jpg</image:loc>
    <image:title>Enamel steel mug in forest green</image:title>
  </image:image>
</url>

The image sitemap format and its full field list are covered in the image SEO guide; the migration-relevant point is that <image:loc> must be the new URL, absolute, and reachable.

  1. Submit the updated sitemap in Search Console under the property for your main site. Resubmission marks the sitemap as freshly fetched and prompts recrawling.

  2. Do not delete the old sitemap history. If your old sitemap is still submitted, updating it in place is fine. What you want is for Google to see the same sitemap URLs now listing new image locations — that is a strong, machine-readable “this moved here” statement.

  3. Verify the new host property in Search Console if you used Strategy B, as noted earlier. Image performance for the new hostname appears under that property’s Performance report, filtered to Search Appearance: Image.

Step 7: Monitor the Migration

Monitoring is where careful migrations prove themselves. Three watches, in priority order.

Search Console image performance

In the Performance report, filter by Search Appearance and select Image. Compare the four weeks before launch to the weeks after, looking at impressions and clicks for image results. Expect a dip during recrawl — a 10–30% impression dip for one to two weeks is typical and recovers. What is not normal:

  • Clicks that recover but impressions that never do (your images rank but for fewer queries — check that alt text survived the migration)
  • A steady decline past week three (check that redirects are 301, not 302, and that new URLs return 200 with correct content type)
  • Zero impressions on the new host property under Strategy B (the host verification or sitemap step failed)

The 404 watch

Tail your logs — origin and CDN — for 404s on image paths:

# nginx: count image 404s by URL, top 20
awk '$9 == 404 && $7 ~ /\.(png|jpe?g|webp|avif|gif|svg)$/ { print $7 }' access.log \
  | sort | uniq -c | sort -rn | head -20

Every 404 on an old image URL is a reference you missed — in a template, a database row, a third-party site, or a cached page. Each one is fixable: add the missing redirect rule or fix the reference. Run this daily for the first two weeks. The count should fall toward zero; anything that persists is a real reference you need to fix at the source.

Most CDNs also expose 4xx rates in their analytics dashboards. Set an alert on the image-path 404 rate so a regression pages you instead of waiting for a customer email.

Uptime and content-type checks

Run a daily synthetic check over a fixed sample of 100 image URLs from your inventory: assert HTTP 200 and a correct Content-Type (an image served as text/html renders broken and is excluded from Google Images). A tiny cron script over the sample catches origin misconfiguration within a day instead of whenever a user complains.

Step 8: Write the Rollback Plan Before You Need It

A rollback plan is cheap insurance. Write it as a runbook before launch, and make sure it answers:

  • What is the trigger? Define it numerically: image search clicks down more than 50% for more than 5 days with no recovery trend, or image 404 rate above 1% of image requests for 48 hours, or any sustained correctness failure (wrong images served).
  • What does rollback mean mechanically? Under Strategy A with DNS-level cutover: flip the CNAME back to the origin, purge caches on both sides, done. Under Strategy B: repoint the old host to serve images again (keep it running in parallel during the migration window), revert the content rewrite with the reverse search-replace command, resubmit sitemaps. This is why the old host must stay live and serving for at least the first month.
  • Who executes it and how long does it take? If rollback takes four hours of coordinated work, it will not happen at 2 a.m. when it is needed. Pre-stage the commands.

The rollback window matters: keep old URLs serving (or redirecting) for at least 90 days. Google’s recrawl of old URLs is spread over weeks, and long-tail references from forums and social media trickle in for months.

The Full Timeline: T-7 to T+30

Here is the whole migration compressed into a checklist you can run.

T-7 days: Inventory and decisions

  • Run the full site crawl; produce the image-URL-to-pages inventory
  • Record baseline: Search Console image performance (last 90 days), current cache headers, current origin load
  • Choose URL strategy: identical paths (Strategy A) unless blocked; document the decision
  • If Strategy B: map every old URL to its new URL; verify the new host property in Search Console
  • Audit current cache headers; decide new Cache-Control policy for the CDN
  • Take a database backup; confirm restore works

T-5 to T-3: Build and stage

  • Configure the CDN: custom domain/CNAME, TLS, cache rules, and the 301 redirect rules for old URLs
  • Stage the content rewrite (WP-CLI dry-run, or the sed commit on staging)
  • Build the cache warmup script from the inventory
  • Test 30 sample URLs end-to-end on staging: 200, correct content type, correct cache headers, correct redirects

T-2 to T-1: Rehearse

  • Full rewrite on staging; re-crawl staging and confirm zero old-host references in HTML, CSS, and JS
  • Warm the staging cache; verify second-request HITs
  • Dry-run the rollback runbook on staging
  • Freeze content changes that add new images (or accept that new images need manual URL updates during the window)

Launch day (T-0)

  • Deploy the content rewrite to production
  • Cut over DNS / CDN routing
  • Purge old URLs from all caches (origin and any intermediate CDN)
  • Run the cache warmup script against production, head of the distribution first
  • Submit the regenerated sitemap in Search Console
  • Smoke test: load the 20 highest-traffic pages in a private browser window with cache disabled; every image renders

T+1 to T+3: Stabilize

  • Daily: 404 watch over logs; fix each hit with a redirect rule or source fix
  • Daily: synthetic sample check on 100 image URLs
  • Verify Search Console is crawling the new URLs (crawl stats or URL inspection on a sample)
  • Watch origin load — an unexpectedly hot origin means the CDN is not caching what you think

T+7: First checkpoint

  • Compare Search Console image impressions and clicks to baseline; a dip is expected, a collapse is not
  • Re-run warmup for any URLs that show repeated MISSes
  • Review CDN analytics for error rates and bandwidth; confirm the offload you expected

T+14: Redirect audit

  • Re-test 50 old URLs with curl -I: all must still 301 correctly
  • Check that no old URL returns 200 with stale content (double-cache check)

T+30: Close-out

  • Full Search Console review against baseline; document the recovery curve for the next migration
  • Confirm image 404 rate near zero
  • Decide the fate of the old host: keep redirects for the full 90 days, then decommission
  • Update internal docs: new canonical image host, cache policy, and the runbook that just worked

Common Questions

Will I lose image rankings during migration? A temporary dip of one to three weeks is typical while Google recrawls. With 301 redirects, identical paths where possible, and an updated image sitemap, rankings historically recover to baseline or better. Without redirects, the old URLs’ signals are lost and recovery depends entirely on fresh ranking of the new URLs.

Can I just leave the old URLs in place and not redirect? Only if the old host keeps serving the images forever. “Not redirecting” is really “keeping two sources of truth,” which guarantees drift: one copy gets updated, optimized, or deleted, and the other serves stale bytes. Redirect and consolidate.

Do I need to change image formats at the same time? Tempting, but no. One variable at a time. Migrating hosts and re-encoding everything to WebP simultaneously makes any regression undiagnosable. Migrate first, verify, then optimize formats as a separate project. If your CDN offers automatic format negotiation, that is the exception — it is transparent to URLs and can ship smaller JPEG and WebP variants without touching your inventory.

How long should I keep the old host? Minimum 90 days after the last old URL stops appearing in your logs. Cheap insurance against the long tail of hotlinks, cached pages, and bookmarks.

Wrapping Up

An image CDN migration fails through omission, not complexity. The teams that lose rankings skipped one of: the inventory, the 301s, the Search Console verification, the sitemap resubmission, or the monitoring. Every one of those is an afternoon of work.

The compressed version: inventory every image URL and the pages that reference it, keep paths identical if you possibly can, 301 everything that changes, rewrite your CMS safely (serialization-aware on WordPress), warm the cache before launch, resubmit an image sitemap, watch Search Console and your 404 logs daily for two weeks, and keep a written rollback plan with a numeric trigger.

Do that, and the migration is boring — which, for a change touching every page of your site, is exactly what you want. If you are still evaluating whether you need a CDN at all, start with the signs you need an image CDN, then compare vendors in the image CDN comparison. And if you are migrating toward a full media platform, Sirv’s media viewer and AI-powered image tools sit on top of the same CDN layer this guide migrates you to — you can try it free and validate the URL strategy on a real account before committing DNS changes.

Related Resources

Format References

Ready to optimize your images?

Sirv automatically optimizes, resizes, and converts your images. Try it free.

Start Free Trial