When both /aboutpage and /aboutpage/ serve content as separate URLs, Google treats them as duplicate pages — ranking signals split between two URLs, link equity diluted, indexing inefficient. The fix is picking one slash convention and enforcing it server-side with a 301 redirect. Either with or without slash is fine; what matters is consistency across the site, the sitemap, internal links, and canonical tags.
curl -I https://example.com/aboutpage curl -I https://example.com/aboutpage/ # Healthy state — one redirects to the other: # /aboutpage → 301 → /about/ (consistent: with slash) # /aboutpage/ → 200 (canonical) # OR # /aboutpage → 200 (canonical) # /aboutpage/ → 301 → /about (consistent: no slash) # Problem state — both serve content: # /aboutpage → 200 # /aboutpage/ → 200 ← duplicate content
# Sitemap URLs curl -s https://example.com/sitemap.xml | grep -oE '[^<]+ ' | head -10 # Mix of with-slash and without is the problem. # Pick one, regenerate sitemap.
| Choose with-slash | Choose no-slash |
|---|---|
| WordPress default | Static site generators (often) |
| Apache/nginx directory-style | Modern frameworks (Next.js default) |
| Convention for content sites | Convention for app-style sites |
Pick whichever your current URLs and inbound links predominantly use. Switching minimises 301 hops and signal disruption.
server {
# Add trailing slash to non-file URLs
rewrite ^([^.]*[^/])$ $1/ permanent;
}
# Note the regex: [^.]*[^/]$ means "no dot in path AND doesn't end in slash"
# Protects file URLs like /style.css from getting a trailing slash
server {
# Strip trailing slash from non-root URLs
rewrite ^(.+)/$ $1 permanent;
# But keep / for root (otherwise it loops)
# The (.+) requires at least one character before /
}
RewriteEngine On
# Add trailing slash to non-file URLs
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} !(.*)/$
RewriteRule ^(.*)$ /$1/ [R=301,L]
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.+)/$ /$1 [R=301,L]
// Force trailing slash example
addEventListener('fetch', event => {
const url = new URL(event.request.url);
const path = url.pathname;
// Skip files (with extension)
const isFile = path.match(/\.[a-z0-9]{2,5}$/i);
// Skip root
if (path === '/' || isFile) {
return event.respondWith(fetch(event.request));
}
// Add slash if missing
if (!path.endsWith('/')) {
url.pathname = path + '/';
return event.respondWith(Response.redirect(url, 301));
}
return event.respondWith(fetch(event.request));
});
# Correct rule excludes file extensions
RewriteCond %{REQUEST_URI} !\.[a-z0-9]+$
RewriteRule ^(.*[^/])$ /$1/ [R=301,L]
# Without the exception, /style.css → /style.css/ → 404
# Settings → Permalinks # Make sure permalink structure ends with / for with-slash convention # Or no / for no-slash # Then run a database search-replace to update any hardcoded links wp search-replace 'href="/seo-audit-platform.html"' 'href="/seo-audit-platform.html"' --skip-columns=guid
# Find inconsistent links in HTML
grep -rE 'href="[^"]+(?<!/)"' ./public/
# Or use the framework's built-in trailing slash setting
# Next.js:
module.exports = { trailingSlash: true }
# Astro:
export default defineConfig({ trailingSlash: 'always' })
# Hugo:
[permalinks]
posts = "/:slug/"
Sitemap URLs must use the canonical convention. Regenerate from CMS or framework. Spot-check:
curl -s https://example.com/sitemap.xml | \ grep -oE '<loc>[^<]+</loc>' | head -20 # All should consistently use (or not use) trailing slash
<!-- canonical tag matches the actual canonical URL form --> <link rel="canonical" href="https://example.com/about-page/" /> <!-- or --> <link rel="canonical" href="https://example.com/about-page" />
If your canonical tags don't match the redirect target, Google gets mixed signals. Fix both in tandem.
You set a server-level redirect, but the CMS or plugin sends conflicting Location headers. Result: chains or loops. Disable plugin-level URL normalisation if you've added server-level rules.
# Bad rule rewrite ^(.+)$ $1/ permanent; # Matches / → / → / → infinite # Good rule rewrite ^(.+[^/])$ $1/ permanent; # Only matches paths that DON'T end in slash, root is excluded
# Bad — drops query string rewrite ^(.+)/$ $1 permanent; # Good — preserves query string (nginx auto-appends unless you use ?) rewrite ^(.+)/$ $1 permanent; # Default behaviour preserves query # Or explicit rewrite ^(.+)/$ $1?$args permanent;
curl -IL https://example.com/about-page curl -IL https://example.com/about-page/ # One should redirect to the other with 301 # The other should return 200