How toAdd URL Normalization Middleware in React Router

Small URL differences can create duplicate pages. /about and /about/ are different URLs, and so are https://www.example.com/about and https://example.com/about.

You can fix this at the edge of your React Router app with middleware. The goal is simple: inspect the incoming request URL, build the canonical version, and redirect before loaders and actions run.

Create the Normalizer

Start with one function that receives a URL and returns the canonical URL. Keeping this logic outside the middleware makes it easy to test without a router.

app/utils/normalize-url.ts
export function normalizeUrl(url: URL) { let normalizedUrl = new URL(url); if (normalizedUrl.hostname.startsWith("www.")) { normalizedUrl.hostname = normalizedUrl.hostname.slice(4); } if (normalizedUrl.pathname !== "/" && normalizedUrl.pathname.endsWith("/")) { normalizedUrl.pathname = normalizedUrl.pathname.replace(/\/+$/, ""); } return normalizedUrl; }

This function removes the www. prefix and strips trailing slashes from non-root paths. It leaves / alone because the root path is already canonical.

Add the Middleware

Now wrap the normalizer in React Router middleware. If the canonical URL differs from the request URL, redirect with HTTP 308 (Permanent Redirect).

app/middleware/normalize-url.ts
import type { MiddlewareFunction } from "react-router"; import { redirectDocument } from "react-router"; import { normalizeUrl } from "~/utils/normalize-url"; export let normalizeUrlMiddleware: MiddlewareFunction<Response> = ({ request }, next) => { let url = new URL(request.url); let normalizedUrl = normalizeUrl(url); if (normalizedUrl.href !== url.href) { throw redirectDocument(normalizedUrl.href, 308); } return next(); };

Using one middleware avoids redirect chains. A request for https://www.example.com/about/ redirects directly to https://example.com/about.

HTTP 308 (Permanent Redirect) keeps the original request method. That is safer than HTTP 301 (Moved Permanently) if a non-GET request reaches a non-canonical URL.

Register It at the Root

Export the middleware from your root route so it runs before every child route.

app/root.tsx
import { normalizeUrlMiddleware } from "~/middleware/normalize-url"; export let middleware = [normalizeUrlMiddleware]; // ... rest of your root route

Root middleware is the right place for URL normalization because the rule is about the whole site, not one page. If a nested route adds its own middleware, React Router will still run the root middleware first.

Invert the Rules

Some sites prefer www. hostnames or trailing slashes. Keep the same middleware shape and change only the utility rules.

app/utils/normalize-url.ts
export function normalizeUrl(url: URL) { let normalizedUrl = new URL(url); if (!normalizedUrl.hostname.startsWith("www.")) { normalizedUrl.hostname = `www.${normalizedUrl.hostname}`; } if (!normalizedUrl.pathname.endsWith("/")) { normalizedUrl.pathname = `${normalizedUrl.pathname}/`; } return normalizedUrl; }

With this version, https://example.com/about redirects to https://www.example.com/about/. Pick one canonical shape and enforce it consistently.

Verify the Redirects

Check the URLs that used to create duplicates:

  • https://www.example.com/about/ should redirect to https://example.com/about
  • https://example.com/about/ should redirect to https://example.com/about
  • https://www.example.com/ should redirect to https://example.com/

This middleware handles the redirect. For SEO, you should still render canonical links in your document head so crawlers and social previews see the same canonical URL in the HTML.