You published a site and /about works, but /about/ is a 404 — or the reverse. Or a page loads with its stylesheet missing on one address and fine on the other. Or the same page is reachable at both addresses.
/about and /about/ are different URLs, and a static host has to pick one as the real address. Most hosts map a file called about.html to the slashless address and a folder about/ holding an index.html to the slashed one — Netlify's default is the exception. Some hosts redirect the other form to the canonical one, some serve both, some answer 404 for one. Pick a form, make the host enforce it where it can, and link to the form that returns 200.
The 30-second version
| Host | about.html |
about/index.html |
The non-canonical form |
|---|---|---|---|
| Cloudflare Workers static assets | /about |
/about/ |
307 to the canonical one (default auto-trailing-slash) |
| Cloudflare Pages | /about (/about.html is redirected there) |
/about/index.html is redirected to /about/ |
status not documented |
| Vercel | /about once cleanUrls: true |
not stated | 308 once trailingSlash is set to true or false; by default (undefined) no redirect is documented here |
| Netlify | a request for /about.html is rewritten to /about/ (Pretty URLs, on by default) |
not stated | /about is forwarded to /about/ |
| Apache | not stated | /about/ |
/about is redirected to /about/ when it is a directory |
| GitHub Pages | not stated | not stated | not stated — measure it |
"Not stated" means the host's documentation page on the topic does not say. Each cell that does is linked in the host sections below. nginx has no row: whether it redirects a slashless folder request depends on how you configure it.
What is actually happening
Relative links resolve against the last slash. RFC 3986 appends a relative reference to all but the last segment of the base URI's path, and its examples resolve g against http://a/b/c/d;p?q to http://a/b/c/g and ./ to http://a/b/c/. So a page at /blog/post that links to style.css asks for /blog/style.css, while the same page at /blog/post/ asks for /blog/post/style.css. Serve a folder's index.html at the slashless address and every relative link on it resolves one level too high.
Apache lists that among the reasons it redirects directory requests: with the redirect, relative URL references inside html pages will work correctly.
Search engines count two addresses. In its own post on the question, Google says it treats each URL separately, whether it is a file or a directory and that it is permissible for the two versions to hold different content. The root is the exception: https://example.com and https://example.com/ are the same. That post is from 2010; for the redirect side, the current guidance is that a permanent redirect is a signal that the target should be canonical. The same page says which URL becomes canonical depends on signals, including whether the redirect was temporary or permanent. So if both forms of an inner page return 200, they are two URLs with the same content; a permanent redirect from one to the other is a stated signal, and a temporary one such as Cloudflare's 307 is a weaker one.
Pick one form
There is no correct answer for the web as a whole. What matters is that links, sitemap, canonical tags and the host's redirects all agree.
- With a slash fits a site built from folders —
blog/index.html. - Without a slash fits a site of flat
about.htmlfiles, and is what Cloudflare's and Vercel's clean-URL options produce.
Then check what your host does with the form you did not pick.
Host by host
Cloudflare Workers static assets and Pages
Workers static assets has an html_handling option. The default, auto-trailing-slash, serves individual files such as foo.html without a trailing slash and folder indexes such as foo/index.html with one, and sends the non-canonical request a 307: its table shows /file/ going to /file and /folder going to /folder/. force-trailing-slash makes every canonical URL end in a slash, drop-trailing-slash makes none do, and none turns the redirects off.
{
"assets": { "html_handling": "drop-trailing-slash" }
}Cloudflare Pages says it will redirect HTML pages to their extension-less counterparts: /contact.html goes to /contact, and /about/index.html goes to /about/. The page gives no status code and does not describe a setting for the slash.
Vercel
Two settings in vercel.json, per its configuration reference. cleanUrls defaults to false; when true it removes the .html extension and answers a request that still has it with a 308. trailingSlash defaults to undefined; set it and the other form gets a 308. Left undefined, the page describes no slash redirect, so check with the command below whether both forms answer 200.
{
"cleanUrls": true,
"trailingSlash": false
}With false, /about/ redirects to /about; with true, /about redirects to /about/, except for paths with a file extension — /about/styles.css is left alone.
Netlify
Pretty URLs is a per-site setting, enabled by default (under Post processing in the site's developer settings): it forwards paths like /about to /about/ and rewrites /about.html to /about/. Netlify links a support guide on altering trailing-slash behaviour from that page; this article does not cover it.
You cannot move the slash with a redirect rule. The same page says so — you cannot use a redirect rule to add or remove a trailing slash — because Netlify matches rules whether or not the path ends in one, and a forced rule like /blog/remove-my-slashes/ /blog/remove-my-slashes 301! is, in its words, an infinite redirect. Redirects in general are a separate topic.
Apache
mod_dir redirects a request for a directory that lacks the slash to the same URL with it. The directive DirectorySlash toggles that, and turning it off may result in information disclosure: with directory listings enabled, /dir/ shows index.html while /dir lists the folder's contents. Leave it on.
nginx
nginx's index directive handles requests that end in a slash; its files are checked in the specified order. How a slashless request for a folder is treated depends on the rest of the configuration, so test it rather than assuming.
GitHub Pages
GitHub's Pages documentation says nothing about the trailing slash, so measure it with the check below.
dropto.run
This host serves from the files you published. Its behaviour below comes from reading the router's path resolution (apps/router/src/resolve.ts, commit 9a4d53a) rather than measuring a live site, and only for the plain static mode — not for single-page-app fallback or documents sites. A path ending in / is looked up only as <path>index.html. A path without a slash is tried as the exact file, then <path>.html, then <path>/index.html, and the first one found is answered with a 200, with no redirect.
So about.html answers at /about and, as far as the code shows, /about/ is a 404; blog/index.html answers at both /blog/ and /blog. The second case is the relative-link problem above: /blog serves the page from the wrong base. So link with root-absolute paths (/style.css, not style.css) on pages you serve from a folder, and link to the form that returns 200. _redirects rules match with or without the slash, as described for redirects generally. Relative paths that break on deep links are a neighbour of this problem: why a site works locally but breaks after deploying.
Check it
Ask each form for its status and where it points, without following redirects:
site=https://example.com # your site
for p in /about /about/ /blog /blog/; do
curl -s -o /dev/null -w "$p %{http_code} %{redirect_url}\n" "$site$p"
doneFor every page you care about, one form should answer 200 and the other a 301, 307 or 308 pointing at it. Two 200s means two URLs for one page. A 404 on either means a link somewhere will break.
Troubleshooting
The stylesheet and images are missing on one address. A relative link on a page served under the wrong form. Use root-absolute paths (/assets/app.css), or make the host redirect to the form the page was written for.
/about/ is a 404 but /about works. The page is a file, about.html, and the slashed lookup only checks for about/index.html. Link to /about, or store the page as about/index.html.
A redirect loop after adding a rule. On Netlify, a forced (!) rule that only adds or removes a slash matches both forms and sends the visitor back to itself. Remove it.
Both addresses show up in search. Both can return 200. Redirect one to the other, and link to the same form everywhere.
A client-side router breaks on the slash. The router sees /about/ as a different route from /about. Configure the router and the host to agree; why a static site 404s on refresh covers the host side.
When not to do this
- Don't switch forms on a live site without redirects. Every link people already hold uses the old form. Change the canonical and redirect permanently from the old one.
- Don't fight the host. If the platform enforces a form, as Netlify's Pretty URLs do by default, follow it rather than writing rules that loop.
- If you can't change the host's behaviour, work around it. On a host that serves both forms, link to one form everywhere and use root-absolute paths.
Questions
Does a trailing slash matter for SEO?
Google's 2010 guidance treats /about and /about/ as separate URLs. If both return the same page with a 200, you have two addresses for one page. Pick one, redirect the other, and link to the same form everywhere. The root URL is the exception: with or without the slash, it is one URL.
Which should I choose, with or without a slash?
Either. Choose the form your host already produces for how your pages are stored, and enforce it.
Why does my page lose its CSS without the slash?
A relative link resolves against everything up to the last slash. At /blog/post the link style.css becomes /blog/style.css; at /blog/post/ it becomes /blog/post/style.css.
What status code does the slash redirect use?
Cloudflare Workers static assets uses 307 and Vercel uses 308. the Cloudflare Pages and Netlify pages cited above do not give one. A 307 is temporary, and Google says whether a redirect is temporary or permanent is one of the signals for which URL is canonical.