A migration spreadsheet can look complete while quietly containing mismatched paths, missing query strings, or unintended hostname changes. Comparing old and new URLs systematically helps you catch these issues before launch. This workflow shows how to compare URL components, review mapping changes, and keep spreadsheet checks separate from redirect testing.
Key Takeaways
- Compare scheme, hostname, port, path, and query string—not just the visible page name.
- Keep the original and proposed URLs alongside a reason for each change.
- Treat normalization as an explicit policy: a trailing slash or query-string change can matter, even when two URLs seem similar.
- Review missing, duplicate, and many-to-one mappings before launch.
- A spreadsheet comparison checks mapping data; it does not prove that redirects work.
Build a migration comparison workflow
Start with a representative, complete inventory of URLs that matter: URLs from the current sitemap, analytics, server logs, internal links, and important external links. Record the source of each URL so you can investigate omissions. A sitemap alone may not include every URL that receives visits or links.
Use one row per old URL and include columns such as:
| Column | Purpose |
|---|---|
old_url | The current absolute URL |
new_url | The intended destination URL |
reason | Why the mapping changes, such as a renamed page or a domain move |
owner | Person responsible for reviewing the mapping |
review_status | Proposed, approved, or needs investigation |
Do not overwrite the old URL when editing a migration plan. Keeping both values makes review, rollback planning, and later redirect checks easier. For large sites, use a consistent export and comparison method rather than manually copying selected rows.
A URL Diff tool can help inspect a pair of URLs, but use the same comparison rules across the full mapping list. A manual spot check is useful for diagnosis; it is not a substitute for checking every row.
Compare URL components deliberately
An absolute HTTP or HTTPS URL can contain a scheme, hostname, port, path, query string, and fragment. Each can change the meaning or behavior of a migration mapping.
| Component | Example difference | Why review it |
|---|---|---|
| Scheme | http://example.com/page → https://example.com/page | HTTPS may be the planned destination, but check that the mapping does not accidentally retain HTTP. |
| Hostname | www.example.com/page → example.com/page | A www change is a domain change. Confirm it matches the chosen canonical hostname and redirect plan. |
| Path | /products/boots → /products/shoes | A path change may be intentional, or it may point to the wrong page. Paths are case-sensitive in many server and application configurations. |
| Trailing slash | /guide → /guide/ | These may be treated as different URLs. Confirm the destination convention and redirect behavior. |
| Query string | /search?q=boots → /search | Removing parameters can discard search state, filters, or tracking data. Decide which parameters are meaningful before removing them. |
A URL’s query string includes everything after ?, up to a fragment marker. Compare the exact query string first, then decide whether a difference is meaningful for the application. For example, ?color=black&size=8 and ?size=8&color=black may be equivalent for one site and not another. Repeated parameters can also be meaningful. Do not sort, drop, or decode parameters automatically without confirming the site’s behavior.
Fragments, such as #details, are not sent to the server in an HTTP request, but may matter to client-side navigation or page sections. Record and review them if the site uses them; do not confuse a fragment change with a server-side redirect destination.
Choose comparison rules before normalizing
Some differences are formatting or case differences in components with case-insensitive rules: schemes and hostnames are case-insensitive. A path is not generally safe to lowercase. Default ports, such as an explicit :443 on HTTPS, can be treated as equivalent to an omitted default port for comparison, but document that choice. Internationalized hostnames may also need a consistent IDNA representation.
Keep both the raw URLs and any normalized comparison values. Normalization should help identify equivalent representations; it should not erase evidence of what was entered in the migration sheet.
Separate intended changes from mapping errors
A difference is not automatically a defect. Classify each row using an approved migration rule, then review exceptions:
- Expected: The hostname changes because the whole site is moving to a new domain.
- Expected: A path changes because a page was renamed or consolidated, with a reviewed destination.
- Needs review: A query string disappears without a stated rule for handling that parameter.
- Likely error: A new URL has an unexpected hostname, typo, casing change, or unrelated path.
- Needs decision: Several old URLs point to one new URL. This can be intentional for consolidated content, but confirm that the destination is relevant to each source.
A reason column should describe the rule, not merely say “redirect.” For example, “old product category retired; map to replacement category” is more reviewable than “301.” Flag rows with blank destinations, blank reasons, duplicate old URLs, duplicate destinations, or unapproved status. Duplicate destinations are not always wrong, but many-to-one mappings deserve human review to ensure they do not send unrelated pages to a generic destination.
Compare mapping rows with a small Python script
For a repeatable component check, save a CSV file named migration.csv with these headers: old_url,new_url,reason. Each URL should be an absolute HTTP or HTTPS URL. The script below reports differences in scheme, hostname, effective port, path, query, and fragment. It also flags repeated source URLs and repeated server destinations. It does not decide whether a change is intentional; reviewers must compare each change with the migration plan.
import csv
from urllib.parse import urlsplit
def url_parts(value):
value = value.strip()
parsed = urlsplit(value)
if parsed.scheme.lower() not in {"http", "https"} or not parsed.hostname:
raise ValueError("expected an absolute HTTP or HTTPS URL")
if parsed.username is not None or parsed.password is not None:
raise ValueError("URLs containing user information are not accepted")
scheme = parsed.scheme.lower()
hostname = parsed.hostname.lower()
try:
port = parsed.port
except ValueError as exc:
raise ValueError("invalid port") from exc
if port is None:
port = 443 if scheme == "https" else 80
return {
"scheme": scheme,
"hostname": hostname,
"port": port,
"path": parsed.path or "/",
"query": parsed.query,
"fragment": parsed.fragment,
}
def server_destination(parts):
# Fragments are excluded because browsers do not send them in HTTP requests.
return tuple(parts[key] for key in ("scheme", "hostname", "port", "path", "query"))
seen_sources = set()
seen_destinations = set()
with open("migration.csv", newline="", encoding="utf-8") as csvfile:
reader = csv.DictReader(csvfile)
required = {"old_url", "new_url", "reason"}
if not required.issubset(reader.fieldnames or []):
raise SystemExit("CSV must include old_url, new_url, and reason headers")
for row_number, row in enumerate(reader, start=2):
old_url = row["old_url"]
new_url = row["new_url"]
reason = row["reason"].strip()
if not old_url.strip() or not new_url.strip():
print(f"Row {row_number}: missing old_url or new_url")
continue
if not reason:
print(f"Row {row_number}: missing reason; review the intended change")
try:
old = url_parts(old_url)
new = url_parts(new_url)
except ValueError as exc:
print(f"Row {row_number}: invalid URL: {exc}")
continue
source_key = tuple(old.values())
destination_key = server_destination(new)
if source_key in seen_sources:
print(f"Row {row_number}: duplicate old URL")
if destination_key in seen_destinations:
print(f"Row {row_number}: destination is used by another mapping")
seen_sources.add(source_key)
seen_destinations.add(destination_key)
differences = [
f"{key}: {old[key]!r} -> {new[key]!r}"
for key in old
if old[key] != new[key]
]
if differences:
print(f"Row {row_number}: " + "; ".join(differences))
else:
print(f"Row {row_number}: URL components match")
For example, comparing http://www.example.com/guide with https://example.com/guide/ reports changes to scheme, hostname, and path. Those differences may all be planned, but the row’s reason and approval should make that clear. The script treats hostnames without regard to case, treats an omitted default port as equivalent to the explicit default, and compares paths, queries, and fragments as written. It is a review aid, not a full URL canonicalizer or a substitute for testing the live site.
Validate redirects separately before launch
A correct-looking mapping does not prove that a server returns the intended response. Before launch, test the old URLs against the staging or production environment appropriate to your plan. Check that each important old URL redirects to its mapped destination, that the response uses the intended redirect status, and that the final destination returns the expected content rather than an error or an unintended redirect chain.
Test representative cases as well as the full list where practical: changed hostnames, paths with trailing slashes, URLs with important query parameters, and URLs that previously had multiple redirect hops. Also review internal links, canonical tags, and XML sitemaps so they point to the intended new URLs. Search-engine migration guidance covers broader tasks such as redirects, site verification, and monitoring; URL comparison is only one part of that work.
Common Mistakes
- Comparing only the domain and path: This misses scheme, port, and query-string changes.
- Lowercasing entire URLs: Hostnames are case-insensitive, but changing path case can break pages on some systems.
- Removing all query parameters: Some are tracking-only; others affect the page or user state. Classify them before deciding.
- Assuming a trailing slash is cosmetic: The server or application may treat slash and no-slash URLs differently.
- Treating every duplicate destination as an error: Consolidation can be deliberate, but each source-to-destination match still needs review.
- Assuming a clean spreadsheet means working redirects: Mapping data and HTTP responses are separate checks.
- Using a URL parser as a security or validity guarantee: Parsing components does not establish that a destination is safe, reachable, or correct.
Best Practices
- Keep a versioned, reviewable mapping file with stable source data and named owners.
- Write down normalization rules before comparing, especially for ports, query parameters, fragments, and internationalized hostnames.
- Preserve raw URLs so a normalized comparison does not hide the original input.
- Review all unexpected differences and all many-to-one mappings with the people responsible for the affected pages.
- Re-run the same comparison after spreadsheet edits and before deployment.
- Pair mapping review with redirect tests, crawl checks, and post-launch monitoring.
Conclusion
Comparing old and new URLs is most useful when it is systematic: inspect each component, apply documented normalization rules, and review every difference against an approved migration reason. Then test the redirects themselves. A migration mapping can describe the intended outcome, but only an HTTP check can confirm what visitors and crawlers actually receive.
References
- Google Search Central: Site moves with URL changes — Official guidance for planning and validating a move that changes URLs. Check the current documentation for the latest steps; its live contents were not verified for this article.
