URL structure for SEO: capitals, underscores, parameters and other traps

Short answer

Good URLs are short, lower case, use hyphens between words, and contain only what changes the page. Google treats URLs as case-sensitive, recommends hyphens over underscores, and advises against session IDs and parameters that don't change the content. Most URL problems matter because they create several addresses for one page. The fix is usually one canonical form, redirects from the others, and internal links that always use it.

A URL with every problem

Problems that create duplicates

These can make one page reachable at several addresses. Search engines then have to work out which is the real one, and links and signals get split between them:

Capital letters Google's URL handling is case-sensitive, so /Coats and /coats are two URLs. Many servers answer both with the same page. Language-region folders such as /en-GB/ use capitals by convention, so SEOSnip doesn't flag them. Just use one form everywhere.
Repeated slashes /coats//wool and /coats/wool usually show the same page, often because a template joins two paths that both end in a slash.
Session IDs A sessionid=, PHPSESSID= or ;jsessionid= in the URL gives every visit a new address, so crawlers find endless copies of the same pages. Google advises against them; keep sessions in cookies.
Tracking parameters on internal links A utm_source= on a link from your own homepage creates another URL for the page, and resets the visitor's real traffic source in analytics.

A canonical tag on each version helps Google pick the right one, but it's a fallback. Fixing the links, and redirecting the wrong versions, is better.

Problems that hurt readability

Underscores Google recommends hyphens: they clearly separate words, while underscores are often used to join them. Use wool-coats, not wool_coats.
Spaces A space becomes %20 in a link, which is ugly to share and easy to break when copied into an email or chat.
Non-ASCII characters Letters such as é or ü are allowed: Google says to percent-encode them in links, and many sites in other languages use them. The risk is the same page linked with and without encoding, or with different encodings. Be consistent.
Very long URLs Google gives no length limit. Long URLs are harder to read, share and remember, and are often a sign of deep folders or stacked parameters. SEOSnip flags URLs over 115 characters as a prompt to look.

Session IDs and tracking parameters

Parameters are the part after ?. Some change the page (?page=2, ?colour=navy) and are fine. Others don't, and Google suggests removing those from URLs where you can:

ParameterChanges the page?What to do
?page=2, ?colour=navyYesKeep it
?sort=priceOrder onlyKeep for visitors, and give the sorted versions a canonical to the unsorted page
?utm_source=…, ?gclid=…, ?fbclid=…NoFine on links from outside (ads, emails); never on your own internal links
?sessionid=…, ;jsessionid=…NoMove sessions to cookies

How to check by hand

1Check this URL and every internal link 30 seconds

Open DevTools (F12), go to the Console and paste:

// Checks this page's URL, and the internal links on it, for the URL problems search engines trip over
const TRACKING = /^(utm_[a-z_]+|gclid|gbraid|wbraid|dclid|fbclid|msclkid|yclid|igshid|mc_cid|mc_eid|_ga|_gl)$/i;
const SESSION = /^(sid|sessionid|session_id|phpsessid|jsessionid|aspsessionid|cfid|cftoken)$/i;
const LOCALE = /^[a-z]{2,3}(-[A-Z][a-z]{3})?-([A-Z]{2}|\d{3})$/; // /en-GB/ style segments aren't capitals problems
const problems = (u, isLink) => {
  const path = u.pathname;
  let decoded = path;
  try { decoded = decodeURIComponent(path); } catch (e) {}
  const letters = path.replace(/%[0-9a-f]{2}/gi, '').split('/').filter(s => !LOCALE.test(s)).join('/');
  const params = [...u.searchParams.keys()];
  const tracking = params.filter(p => TRACKING.test(p));
  return [
    /[A-Z]/.test(letters) && 'capitals',
    /\s/.test(decoded) && 'spaces',
    /\/\/+/.test(path) && 'double slashes',
    /_/.test(path) && 'underscores',
    /[^\x00-\x7F]/.test(decoded) && 'non-ASCII characters',
    (u.origin + path).length > 115 && 'long (' + (u.origin + path).length + ' characters)',
    (params.some(p => SESSION.test(p)) || /;jsessionid=/i.test(u.href)) && 'session ID',
    // Tracking parameters only matter on your own links; on the page's address they just mean a campaign link was used
    isLink && tracking.length && 'tracking: ' + tracking.join(', '),
  ].filter(Boolean).join('; ');
};
console.table({ 'This page': { url: location.href, problems: problems(new URL(location.href), false) || 'none' } });
const site = location.hostname.replace(/^www\./, '');
const links = [...new Set([...document.querySelectorAll('a[href]')]
  .filter(a => typeof a.href === 'string') // skips SVG links, whose href isn't a string
  .map(a => a.href))]
  .map(href => { try { return new URL(href); } catch (e) { return null; } })
  .filter(u => u && /^https?:$/.test(u.protocol) && u.hostname.replace(/^www\./, '') === site);
const flagged = links.map(u => ({ link: u.href.slice(0, 100), problems: problems(u, true) })).filter(r => r.problems);
if (flagged.length) console.table(flagged.slice(0, 50));
console.log(flagged.length + ' of ' + links.length + ' internal links have URL problems');

It checks this page's address, then every link on the page to the same site, for capitals, spaces, repeated slashes, underscores, non-ASCII characters, length, session IDs and tracking parameters.

It uses the same rules as SEOSnip, including skipping /en-GB/ style folders, and also runs the page checks on every internal link (SEOSnip checks those on the page's own address only). Tracking parameters only count on links, not on the address you arrived at. It only sees links in the page as rendered; it makes no requests.

2Test the variants 2 minutes

Try the page's address in capitals, with a doubled slash, and with an extra ?utm_source=test. Each should either redirect to the real address or have a canonical pointing to it. If they all load as separate pages with no canonical, they're duplicates.

3Look at what Google has indexed 5 minutes

In Search Console, the Pages report lists URLs under reasons such as "Duplicate without user-selected canonical" or "Alternate page with proper canonical tag". Scan those lists for capitals, parameters and session IDs.

How to fix them

  1. Pick one form: lower case, hyphens, no tracking or session parameters. Use it in every internal link, the sitemap and canonicals.
  2. Redirect the other forms with a 301: capitals to lower case (except language folders such as /en-GB/), repeated slashes to single ones.
  3. Strip tracking from internal links. Track internal clicks with analytics events instead.
  4. Move sessions to cookies. If you can't yet, make sure every page has a canonical without the session ID.
  5. Don't rename URLs just to tidy them. A working URL with an underscore is better than a broken one. Fix new URLs going forward, and redirect old ones only when you're changing them anyway.

How crawlers report this

If a crawl sent you here, these are the labels to look for. Our guide to Screaming Frog and Sitebulb issues covers other common ones.

Screaming FrogSitebulbWhat it means
URL: UppercaseURL contains upper case charactersCapitals can create a second version of the same page.
URL: Contains A SpaceURL contains whitespaceThe URL has a space, which should be encoded or removed.
URL: Multiple SlashesURL contains a double slashA repeated slash, usually from joining URL parts in code.
URL: Non ASCII CharactersURL contains non-ASCII charactersCharacters such as accents need encoding. This is normal on many non-English sites.
URL: UnderscoresNo direct matchThe URL uses underscores, not hyphens.
URL: Over 115 CharactersNo direct matchThe URL is long. Treat this as a tidiness note.
URL: GA Tracking ParametersQuery string contains tracking parametersInternal links carry tracking parameters, creating extra URLs to crawl.
No direct matchQuery string contains session ID parametersLinks include a session ID, so every visit can create new URLs.

Doing this on every page?

SEOSnip checks URLs for you

Open the extension on any page and it checks the page's address and its internal links. You get:

  • A flag for each problem in the page's own URL: capitals, spaces, repeated slashes, underscores, non-ASCII characters, or over 115 characters, with the problem marked in the URL
  • Session IDs in the page's URL or its internal links
  • Internal links that carry tracking parameters, with the parameter marked and the link text
  • A ticket with that evidence, and a "Check if fixed" button that re-runs the check