URL structure for SEO: capitals, underscores, parameters and other traps
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
https://example.com/Coats/Wool_coats//navy%20coat?sessionid=8f3a&utm_source=newsletter
- C, W capital letters
- _ underscore between words
- // repeated slash
- %20 a space
- sessionid= a session ID
- utm_source= a tracking parameter (a problem when your own links use it)
https://example.com/coats/wool-coats/navy-coat
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:
/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./coats//wool and /coats/wool usually show the same page, often because a template joins two paths that both end in a slash.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.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
wool-coats, not wool_coats.%20 in a link, which is ugly to share and easy to break when copied into an email or chat.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:
| Parameter | Changes the page? | What to do |
|---|---|---|
?page=2, ?colour=navy | Yes | Keep it |
?sort=price | Order only | Keep for visitors, and give the sorted versions a canonical to the unsorted page |
?utm_source=…, ?gclid=…, ?fbclid=… | No | Fine on links from outside (ads, emails); never on your own internal links |
?sessionid=…, ;jsessionid=… | No | Move 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
- Pick one form: lower case, hyphens, no tracking or session parameters. Use it in every internal link, the sitemap and canonicals.
- Redirect the other forms with a 301: capitals to lower case (except language folders such as
/en-GB/), repeated slashes to single ones. - Strip tracking from internal links. Track internal clicks with analytics events instead.
- Move sessions to cookies. If you can't yet, make sure every page has a canonical without the session ID.
- 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 Frog | Sitebulb | What it means |
|---|---|---|
| URL: Uppercase | URL contains upper case characters | Capitals can create a second version of the same page. |
| URL: Contains A Space | URL contains whitespace | The URL has a space, which should be encoded or removed. |
| URL: Multiple Slashes | URL contains a double slash | A repeated slash, usually from joining URL parts in code. |
| URL: Non ASCII Characters | URL contains non-ASCII characters | Characters such as accents need encoding. This is normal on many non-English sites. |
| URL: Underscores | No direct match | The URL uses underscores, not hyphens. |
| URL: Over 115 Characters | No direct match | The URL is long. Treat this as a tidiness note. |
| URL: GA Tracking Parameters | Query string contains tracking parameters | Internal links carry tracking parameters, creating extra URLs to crawl. |
| No direct match | Query string contains session ID parameters | Links 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
Internal links carry tracking parameters
| Link | Found at | Link text |
|---|---|---|
https://example.com/coats?utm_source=homepage | main > section > a | “Shop coats” |