Trailing slashes and letter case: the small decisions that matter
You've built a solid product. Your API is fast, your database is optimized, and your content is ranking. Then one day you notice something odd: the same page is showing up in Google Search Console twice—once as /products and again as /products/. Or your analytics shows traffic split across /Blog/Article and /blog/article. These aren't edge cases. Trailing slashes and letter case are two of the most common URL normalization problems I've debugged across the platforms I've built. The fix is simple, but the impact on search visibility is real.
Why /page and /page/ are different URLs
To search engines and web servers, /page and /page/ are completely separate URLs. They have different paths. When a crawler visits both, it sees them as distinct resources, which means:
- Link equity gets split. If you have both URLs live, inbound links scatter across two versions instead of consolidating on one.
- Duplicate content penalties apply. Search engines may penalize you for hosting the same content at two addresses.
- Your analytics get messy. You'll see traffic split across versions, making it harder to measure what actually works.
- Users get confused. Sharing a link might land visitors on a different-looking URL than they expect.
I learned this the hard way on a travel platform we built. Our Next.js router naturally created pages at both /destinations and /destinations/ depending on how users navigated. Within weeks, Search Console showed duplicate content warnings, and our organic traffic started to plateau. Once we enforced one format across the board, we consolidated signals and saw a measurable recovery.
The HTTP spec treats them as different. From a technical standpoint, RFC 3986 doesn't mandate that /page and /page/ are the same—it's up to the server. Most web frameworks are agnostic; they'll serve content at both unless you explicitly redirect one to the other.
Redirect the non-preferred version with 301
Once you decide which format you prefer, you need to redirect all traffic from the other version. A 301 Moved Permanently redirect tells search engines that the resource has permanently moved to a new address. This is critical because it passes link equity and ranking signals from the old URL to the new one.
If you prefer trailing slashes (for example, /page/), any request to /page should return:
HTTP/1.1 301 Moved Permanently
Location: /page/
Content-Length: 0If you prefer no trailing slash (for example, /page), the redirect reverses:
HTTP/1.1 301 Moved Permanently
Location: /page
Content-Length: 0In a Next.js application, I'd handle this at the page level or using middleware. Here's a middleware approach:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
// Redirect /page/ to /page (remove trailing slash)
if (pathname !== '/' && pathname.endsWith('/')) {
return NextResponse.redirect(
new URL(pathname.slice(0, -1), request.url),
{ status: 301 }
);
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!api|_next|static).*)'],
};For a NestJS backend, you'd handle it in a global guard or middleware:
import {
Injectable,
NestMiddleware,
} from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class TrailingSlashMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const pathname = req.path;
// Remove trailing slash (except for root)
if (pathname !== '/' && pathname.endsWith('/')) {
return res.redirect(
301,
pathname.slice(0, -1)
);
}
next();
}
}The key is to be consistent: pick one style and enforce it everywhere. I've seen teams use trailing slashes on static pages but not API routes, which creates confusion and splits signals. Decide once, configure it once, and move forward.
Lowercase all paths
URLs are case-sensitive by specification, but in practice, most users and systems treat them as case-insensitive. Unfortunately, search engines don't. /Blog/Article and /blog/article are two different URLs to Google.
My recommendation: lowercase everything. It's simpler, more user-friendly, and matches the behavior users expect. Nobody types capital letters into the address bar.
On the CRM platform I built, we inherited a codebase where some routes were defined as /Contacts and others as /contacts. This created duplicate content issues and made it hard to reason about the system. When we normalized everything to lowercase, our search coverage improved and the codebase became more predictable.
Implement case-normalization at the middleware level, before routing logic runs:
// NestJS example
@Injectable()
export class LowercaseUrlMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const pathname = req.path;
const lowercase = pathname.toLowerCase();
if (pathname !== lowercase) {
return res.redirect(301, lowercase + req.url.slice(pathname.length));
}
next();
}
}In Next.js, you can do the same in middleware:
// middleware.ts
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
const lowercase = pathname.toLowerCase();
if (pathname !== lowercase) {
const url = request.nextUrl.clone();
url.pathname = lowercase;
return NextResponse.redirect(url, { status: 301 });
}
return NextResponse.next();
}Alternatively, if you run behind a reverse proxy (nginx, Cloudflare, AWS ALB), handle normalization there to avoid unnecessary application-level overhead:
# nginx example
server {
listen 80;
server_name example.com;
# Lowercase URL normalization
if ($request_uri ~ ^(.*)([A-Z])(.*)$) {
return 301 $scheme://$server_name$request_uri_lowercased;
}
location / {
proxy_pass http://app_backend;
}
}Configure once in your framework or proxy
The most efficient approach is to handle URL normalization at a single layer—either in your framework or at your edge (proxy, CDN, API gateway). This prevents bugs where different parts of your system normalize differently.
I prefer the proxy approach when possible because it centralizes the logic, removes load from your application servers, and is faster (since redirects are cached at the edge). If you're running on AWS, you can use Application Load Balancer rules. If you're on Cloudflare, you can write a Worker. On Vercel, middleware works well.
The pattern is always the same: decide on your standard (lowercase, no trailing slash is my default), write one normalization handler, and apply it to all incoming requests.
Here's what that looks like in a real system. On the ride-hailing platform we built, we had a Next.js frontend and a NestJS API. We normalized URLs at the API Gateway (AWS ALB) and in Next.js middleware. This ensured that whether users hit the site directly or through a shareable link, they always landed on the canonical URL.
Common mistakes
- Forgetting to normalize query strings.
/page?sort=dateand/page?sort=DATEare different. Normalize the entire URL, not just the path. - Redirecting the wrong direction. If your site was historically available at
/page/, redirecting all traffic to/pagebreaks backlinks and passes redirect chains. Choose based on what already has equity. - Mixing normalization layers. If your CDN normalizes to lowercase but your app normalizes to UPPERCASE, you'll get duplicate content. Pick one layer and own it.
- Using 302 instead of 301. A 302 Found is temporary; search engines will keep both URLs in their index. Use 301 to consolidate ranking signals.
- Normalizing after routing. If you normalize in business logic instead of middleware, you risk serving content from the wrong route handler, complicating your codebase.
Key takeaways
/pageand/page/are different URLs to search engines; they split link equity and cause duplicate content issues.- Use 301 redirects to enforce one format; pick the version that already has the most inbound links or traffic.
- Lowercase all paths. It's simpler, more user-friendly, and avoids case-sensitivity bugs.
- Handle normalization at a single layer—middleware in your framework, or rules in your proxy/CDN.
- Configure it once and monitor Search Console to confirm consolidation within weeks.






Comments
Loading comments…