This is a hypothetical case study. The company and numbers are invented to illustrate a very common migration problem.

The setup

"Hollow Oak Furniture" is a small online furniture shop. For twelve years, its site ran on a Windows server: a mix of old hand-written HTML pages, a product catalogue, and thousands of product images uploaded by different staff over the years.

The site is moving to a modern Linux host. The migration plan looks careful: copy all files, export the database, test the homepage, the product pages and checkout. Everything works on the staging site. The switch happens on a Tuesday morning.

By Tuesday afternoon, customers are emailing about missing images, and some older blog posts show broken pages.

What went wrong

Windows file systems (as typically configured) and IIS treat file and URL paths as case-insensitive. On the old server, all of these returned the same file:

/Images/Chairs/Oak-Chair.JPG /images/chairs/oak-chair.jpg /IMAGES/CHAIRS/OAK-CHAIR.JPG 

Linux file systems are case-sensitive. On the new server, only the exact spelling of the file on disk works. Everything else is a 404.

Over twelve years, nobody had been consistent:

  • Some staff uploaded Oak-Chair.JPG, then linked to oak-chair.jpg in a blog post. It always worked, so nobody noticed.
  • Old HTML templates referenced /Images/ while newer ones used /images/.
  • Other websites linked to product pages with whatever capitalisation they'd copied.

Staging testing missed it because testers clicked through the current navigation, which happened to use consistent paths. The inconsistent links were in old content and external links.

Step 1: Measure the damage

The team starts with the new server's access logs, which show the actual 404s users and bots are hitting:

# Top 404 paths since the migration (combined log format) awk '$9 == 404 { print $7 }' /var/log/nginx/access.log \ | sort | uniq -c | sort -rn | head -50 

Then they crawl the whole site with a link checker to find every broken internal link, not just the ones visited today. In this hypothetical, the result is about 1,900 broken links, almost all images and old pages.

Step 2: Confirm it's a case problem

For each 404 path, check whether a file exists with a different capitalisation:

#!/usr/bin/env python3 """case_mismatch.py: map 404 paths to real files that differ only by case.""" import os, sys WEB_ROOT = "/var/www/site" # Build a lowercase -> actual path index of every file index = {} for root, _, files in os.walk(WEB_ROOT): for f in files: full = os.path.join(root, f) rel = "/" + os.path.relpath(full, WEB_ROOT).replace(os.sep, "/") index.setdefault(rel.lower(), []).append(rel) for line in sys.stdin: # one 404 path per line  path = line.strip().split("?")[0] matches = index.get(path.lower()) if matches and path not in matches: status = "AMBIGUOUS" if len(matches) > 1 else "FIXABLE" print(f"{status}\t{path}\t{matches[0]}") 
awk '$9 == 404 { print $7 }' access.log | sort -u | python3 case_mismatch.py > mismatches.tsv 

In the hypothetical, over 90% of the broken links are FIXABLE: the file exists, just with different capitals. A handful are AMBIGUOUS: two files differing only by case (Sofa.jpg and sofa.jpg), which could co-exist on Linux but never on the old server. Those need a human.

Step 3: Pick a fix strategy

The team considers three options:

Option A: make the web server case-insensitive. Some servers and modules can do case-insensitive file matching. It's quick, but it hides the problem and adds a file-system lookup to every 404. It also means the same page is reachable at many URLs.

Option B: lowercase everything. Rename every file to lowercase, update every reference, and redirect any mixed-case URL to its lowercase version. Clean and consistent, but more work.

Option C: redirect only the known mismatches. Generate a redirect map from mismatches.tsv. Precise, but doesn't catch future mistakes.

They choose B for the long term, with C as an immediate fix the same afternoon.

Immediate fix: a generated redirect map

awk -F'\t' '$1 == "FIXABLE" { printf " %s %s;\n", $2, $3 }' mismatches.tsv > case-redirects.map 
map $uri $case_redirect { default ""; include /etc/nginx/case-redirects.map; } server { # ... if ($case_redirect) { return 301 $case_redirect; } } 

Long-term fix: lowercase everything

  1. Rename files to lowercase (after resolving the ambiguous pairs by hand).
  2. Rewrite references in templates, the database and blog content with a careful search-and-replace, reviewed in a diff.
  3. Add a rule that redirects any URL containing capital letters (under /images/ and old content paths) to its lowercase version, so external links keep working.
  4. Add a check to the upload process that lowercases new file names automatically.

Step 4: Verify and keep verifying

After the changes, the team re-crawls the site and confirms internal broken links are back to near zero. They keep watching the 404 log for a few weeks for external links they didn't know about, and set up a scheduled crawl with linkbruno.com so new breakage shows up within days instead of via customer emails.

Lessons

  • Case-insensitive servers hide inconsistent links for years. The migration doesn't create the problem; it reveals it.
  • Test with old content and real traffic, not just current navigation. Replaying a sample of real request paths from old logs against staging would have caught this before launch.
  • Decide on a URL convention (lowercase is the common choice) and enforce it at upload time.
  • Use logs and a full crawl together. Logs show what people hit; a crawl shows everything that's broken.

Related reading

Have you been bitten by case sensitivity during a migration? Did you go case-insensitive, lowercase everything, or redirect the known paths? Tell me in the comments.