← Back to resources

How-To//6 min read/Jason Gordon

Trailing Slashes: The URL Decision Nobody Makes

/pricing and /pricing/ are two different URLs to your server, to Google and to your analytics. Most sites never pick one. Here is why it costs you, which form to choose, how we set it on Vercel and in custom code, and a curl loop that audits any site in ten minutes.

Trailing Slashes: The URL Decision Nobody Makes

companya.com/pricing and companya.com/pricing/ are two different URLs. Your server thinks so, Google thinks so, your analytics thinks so. Most sites never pick one, so the framework picks for them, inconsistently, and nobody notices until the numbers stop adding up.

Here is the whole answer up front: pick one form, point every internal link straight at it, and 301 the other. Neither form is better. Being inconsistent is what costs you.

They are genuinely two different URLs

A URL path is a literal string, and /pricing is not the same string as /pricing/. That is RFC 3986, the spec every browser, CDN and crawler implements.

There is exactly one exception. At the root, companya.com and companya.com/ are the same URL by spec, because an empty path is defined as equivalent to /. Every other path in your site is not covered by that rule.

The convention comes from the filesystem era. A trailing slash meant a directory and Apache would serve its index file. No slash meant a file. Modern frameworks resolve routes from a table, not a disk, so the distinction carries no meaning anymore. It just carries consequences.

Google's Search Central team has held the same position since their 2010 post To slash or not to slash: the two forms are separate URLs that can serve different content, there is no ranking preference between them, and the job is to pick one and be consistent.

The bug almost nobody knows about

Relative links resolve differently depending on the slash, and this breaks real sites. The algorithm is defined in RFC 3986 section 5, Reference Resolution: a relative path is merged against the base URL with everything after the last slash discarded. The trailing slash is what decides where that last slash falls.

Put <a href="page-two"> in your template. On /page it resolves to /page-two. On /page/ it resolves to /page/page-two. Same HTML, same template, two different destinations.

The same applies to relative asset paths. <img src="logo.png"> on /about pulls /logo.png. On /about/ it pulls /about/logo.png, which is a 404.

This is why a site can look perfect on the canonical form and quietly 404 half its assets on the other. If you have ever chased a bug that only reproduces on "some pages" and never on the homepage, check the slash before you check anything else.

Four costs, all real

Inconsistency is not a tidiness problem. It has a bill attached.

CostWhat happens
Split analytics/pricing and /pricing/ appear as two rows in GA4. Every report on that page is understated.
Split crawl and link equityGoogle crawls both, indexes one, and backlinks pointing at the wrong form pass through a redirect.
A redirect hop on every internal clickEach hop is an extra round trip before any content arrives. Users on mobile networks pay the most.
Broken relative links and assetsPer the section above. Silent, and it looks like a random bug.

The redirect hop deserves a note on honesty. A redirect adds a full request-response cycle before the real page starts loading, and Catchpoint's Web Performance 101 documents one page where 46 redirects contributed to an almost 13-second load. What nobody has published is a clean per-redirect millisecond figure, because it depends on connection reuse, protocol and network. Treat it as one avoidable round trip, not as a number you can quote.

Which form should you pick?

Whichever one your site already uses. This is not a taste decision and there is no upside to switching.

  • Existing site with search history: match what Google has already indexed. Check Search Console, not your preference.
  • Migrating from WordPress: WordPress defaults to trailing slash (how we move WordPress clients), so keep the slash unless you enjoy remapping every URL you own.
  • New build: match your framework default. Next.js and Vite ship slash-free.
  • Never switch an established form for aesthetics. The migration cost is real. The gain is zero.

Company A runs a WordPress site at companya.com/services/ with four years of Analytics history. They rebuild on Vercel. The correct answer is trailing slash, because everything they already own points there. What the new stack would have preferred is irrelevant.

How we do it on Vercel

One config flag, and Vercel handles the redirect at the edge (why we host there). It lives in vercel.json or its TypeScript equivalent.

{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "trailingSlash": true
}

Set true and paths without a slash redirect to the slash form. Set false and the reverse. Four things worth knowing:

  1. Vercel issues a 308, not a 301. Both are permanent, Google treats them the same, and 308 additionally preserves the request method and body. A 301 historically let clients downgrade POST to GET. Query strings and UTM parameters survive either way.
  2. trailingSlash: true excludes paths with file extensions. /sitemap.xml does not become /sitemap.xml/.
  3. Do not set this in two places. If you are on Next.js, next.config.js also has a trailingSlash option. Pick one layer. Conflicting values produce redirect loops.
  4. Legacy URL maps go in bulk redirects, not into vercel.json. vercel redirects upload redirects.csv handles thousands of rules without a deployment. Nobody should be hand-writing 800 redirect entries.

For static HTML, cleanUrls: true strips .html extensions and redirects to the extensionless path. Set it alongside trailingSlash, not instead of it.

How we do it in custom code

The principle is the same everywhere: normalise once, at the edge, and normalise your internal links at build time.

  • Nginx: rewrite ^(.*[^/])$ $1/ permanent; to add, or rewrite ^(.*)/$ $1 permanent; to remove.
  • Apache: DirectorySlash On plus a RewriteRule in .htaccess.
  • Cloudflare: a Redirect Rule or a Worker on the path, which puts it ahead of your origin entirely.
  • Express: one middleware before your router, comparing req.path and issuing res.redirect(308, ...).
  • Astro, Hugo, Eleventy: all three have a trailingSlash build option. Use it rather than post-processing.
  • Nginx reference: the rewrite directive docs cover flag behaviour if you need anything beyond permanent.

The internal links are a separate job, and it is a source change, not a redirect. Rewrite them at the link layer, not by hand. One function inside your <Link> component that normalises every internal path on output, plus the same function in your sitemap generator and your canonical tag, means nothing can drift back. Editing 800 href attributes by hand guarantees the 801st is wrong by Friday.

The nine places the form has to match

This is the part teams skip, and it is why sites stay half-migrated for years.

  1. Every internal href
  2. The <link rel="canonical"> tag, and each page must canonical to itself, not the homepage
  3. Every <loc> in sitemap.xml
  4. Any path referenced in robots.txt
  5. hreflang annotations
  6. @id and url fields in your JSON-LD
  7. Your Search Console property and any URL inspections
  8. GA4 and GTM configs, plus every ad landing URL you are paying for
  9. The redirect rule catching the other form

One more: collapse your redirect chains. http://companya.com/pricing going to HTTPS, then to www, then to the slash form is three hops where one will do. Write the rule so the first response lands on the final URL.

The canonical-to-homepage mistake in item 2 is worth calling out on its own. It is the single most common trailing-slash-adjacent bug I see in audits, usually on campaign landing pages, and it actively tells Google to drop the page from the index.

Audit any site in ten minutes

Run this against your own domain. It tells you the current state before you change anything.

for p in / /pricing /about /blog; do
  echo "== $p"
  curl -sI "https://companya.com$p"  | head -2
  curl -sI "https://companya.com$p/" | head -2
done

You want exactly one form returning 200 and the other returning 301 or 308. Two 200s means both forms are live and you are splitting everything. Two redirects means you have a loop.

Then check three more things: Search Console coverage for both forms appearing as separate URLs, your GA4 page-path report for duplicate rows, and a crawl of your sitemap confirming every <loc> matches the form that returns 200.

One rule to take away. Pick a form. Point every internal link straight at it. Redirect the other. It is an afternoon of work that most sites never do, and it quietly costs them for years.

We handle this as standard on every SpecLoop build. If you want to know what your current site is doing, the curl loop above will tell you in about a minute.

Sources & references

Frequently asked

Questions people ask about this

  • No. Neither form ranks better. Google treats them as two distinct URLs with no preference between them. Consistency is the only thing that affects your results.

About the author

Jason Gordon

Founder, SpecLoop

Jason Gordon is the founder of SpecLoop, a spec-driven AI development studio that builds production-grade business applications from a complete specification, in weeks, at flat rate, with 100% code ownership. He also builds GeoTest, a free tool that scores how visible a site is to AI search engines. He writes about AI search, spec-driven development, and why most AI builds never reach production.

Keep reading

Next step

Want a spec for your build?

We write the full specification before any code is generated, then ship in 30–60 days at one flat rate. You own every line.