Trailing Slashes on URLs: Contentious or Settled?
After some discussion with Salma last week, I decided it was worthwhile to do a deep dive on Trailing Slashes in URLs. More specifically, which of these should I be using?
http://zachleat.com/resource
http://zachleat.com/resource/
I did what any curious but self-doubting person might do in this situation. I posted a Twitter poll. The results surprised me!
But before we go much further, let’s go over the problems we’re trying to solve:
- Performance: when you leave off a trailing slash and the platform expects one (or vice versa), you get a redirect which is a performance no-no.
- SEO: if your content exists at two (or more!) distinct URL endpoints, it is a SEO no-no. SE-no-no. SEO-apolo-graphql-anton-ohno (I apologize for nothing).
*Ahem*
. You need redirects. - Asset References: if your markup uses relative paths to reference assets (e.g.
<img src="image.avif">
), these URLs may break if your host isn’t aggressive enough with redirects to a canonical home base. - Cool URIs Don’t Change: we want to avoid including any file extension in our URLs.
At the end, the most important piece to remember here is that consistency is king. No matter which approach you use for a specific resource (trailing slash or sans the slash), it should be the canonical version and it should be used everywhere (even when third parties link to your site). Any other non-canonical version of the URL should (ideally) redirect to the canonical version.
Interestingly, some of my surprise at current sentiment was that developers sometimes use different strategies for different types of content within the same project! That was something I did not expect and am curious how well that is supported by tooling.
Perspectives
I think the leaky part of the poll in question is that there are a bunch of different perspectives to this problem:
- Developers, wanting to implement a personal or team preference.
- App/site/framework tooling (e.g. say, uh, you’re the maintainer of Eleventy)
- Platform (e.g. Netlify—casting a wide net and thinking what works best across as many tools and frameworks as possible)
Disclosure: I am both an employee of Netlify and the creator/maintainer of Eleventy.
Sebastien Lorber has put together an incredible repository of research results showing how this works on a variety of popular hosts and static site generators. I’ll reference this data throughout this post. Sebastien also included results for a variety of different configuration options on those different platforms. I simplified to platform-default behavior for this post.
Writing resource/index.html
Gatsby, Docusaurus, NuxtJS, and Eleventy all use folder generated resource/index.html
files to offer an easy and portable way to use trailing slashes by default.
The default filename index.html
is a convention that’s pretty safely cemented in web history at this point. It represents the file shown when a file name is not specified in the URL. Citations from Apache, NGINX, LiteSpeed, Microsoft IIS.
Here’s what happens when a web browser makes a request to a URL representing this content:
/resource
- ✅ GitHub Pages, Netlify, and Cloudflare Pages redirect to the trailing slash
/resource/
as expected. - 🟡 Warning: Vercel, Render, and Azure Static Web Apps: slashless
/resource
returns content but without redirects, resulting in multiple endpoints for the same content.
- ✅ GitHub Pages, Netlify, and Cloudflare Pages redirect to the trailing slash
/resource/
- ✅ All hosts agree that
/resource/
should return content fromresource/index.html
- ✅ All hosts agree that
- 💔 Warning: If you’re using relative resource URLs, the assets may be missing on Vercel, Render, and Azure Static Web Apps (depending on which duplicated endpoint you’ve visited).
<img src="image.avif">
on/resource/
resolves to/resource/image.avif
<img src="image.avif">
on/resource
resolves to/image.avif
Writing resource.html
Both Jekyll and Next.js take a different approach. They output resource.html
instead of index.html files.
Here’s what happens when a web browser makes a request to a URL representing this content:
/resource
- ✅ Almost everyone agrees that
/resource
should return content fromresource.html
- 🆘 Warning: Confusingly Vercel is the only host tested that returns a HTTP 404 error for
/resource
.
- ✅ Almost everyone agrees that
/resource/
- ✅ Netlify and Cloudflare Pages redirect to the slashless
/resource
. - 🆘 Warning: GitHub Pages, Vercel, and Azure Static Web Apps all return a HTTP 404 error. I’ll admit this one is a little more contentious. I won’t take a hardline here—I can see the reasoning behind it. But I do consider it better to redirect than 404.
- ✅ Netlify and Cloudflare Pages redirect to the slashless
⚠️ Writing Both resource.html
and resource/index.html
There exists an even edgier edge case here. What happens when resource.html
and resource/index.html
both exist in a project?
/resource
- ✅ Everyone agrees that
/resource
should return content fromresource.html
- ✅ Everyone agrees that
/resource/
- ✅ Almost everyone agrees that
/resource/
should return content fromresource/index.html
- 🆘 Warning: Netlify redirects to
/resource
instead.
- ✅ Almost everyone agrees that
More seriously, I think this case actually represents a larger URL usability problem for the content. In this case, though pedantically and technically correct, /resource
and /resource/
confusingly resolve to different pieces of content. I think this should be avoided if at all possible and a tooling error is warranted. It could be argued that Netlify takes an opinionated stance here to attempt to resolve the ambiguity at a platform level.
Eleventy users can rest easy: because input files resource.html
and resource/index.html
both write to the output directory at _site/resource/index.html
by default, we throw a DuplicatePermalinkOutputError
error to mitigate this for you. (You can force the issue using permalink
if you really want)
Results Table
Here’s a summary table of the above findings, leaving off the (in my opinion) flawed Writing Both case above.
Legend:
- 🆘 HTTP 404 Error
- 💔 Potentially Broken Assets (e.g.
<img src="image.avif">
) - 🟡 SEO Warning: Multiple endpoints for the same content
- ✅ Correct, canonical or redirects to canonical
resource.html |
resource/index.html |
|||
---|---|---|---|---|
Host | /resource |
/resource/ |
/resource |
/resource/ |
GitHub Pages | ✅ | 🆘 404 |
✅➡️ /resource/ |
✅ |
Netlify | ✅ | ✅➡️ /resource |
✅➡️ /resource/ |
✅ |
Vercel | 🆘 404 |
🆘 404 |
🟡💔 | ✅ |
Cloudflare Pages | ✅ | ✅➡️ /resource |
✅➡️ /resource/ |
✅ |
Render | ✅ | 🟡💔 | 🟡💔 | ✅ |
Azure Static Web Apps | ✅ | 🆘 404 |
🟡💔 | ✅ |
So, what?
Ideally, (speaking as the maintainer of Eleventy) folks working on developer tooling should craft tools to create output that uses existing conventions and can be portable to as many hosts in as many different hosting environments as possible.
That being said, given the above information it seems clear to me that resource/index.html
is marginally safer than resource.html
for tooling (on the premise that resolved but duplicated content with potentially missing assets is better than a 404 error 😅).
What’s more, I think it is the unique job of our development tools to help diagnose and mitigate future production problems. My (very biased) opinion is that more frameworks and tools should take a harder line in preventing confusingly similar but distinct URLs in a project. It is a usability error to have resource.html
(output to /resource
) and resource/index.html
(output to /resource/
) fighting over the same URL in the same project, and we should treat it as such.
43 Comments
@sil
A small mitigating factor; it is in my opinion easier to set or use a new default page for a directory (if you want your pages to be PHP, say) by writing /resource/index.php than it is to make /resource resolve to /resource.php. (Doesn't apply as much to SSG, of course!)
@zachleat
This post would not have been possible without this excellent research from @sebastienlorber github.com/slorber/traili…
@zachleat
Completely agree!
@colbyfayock
oooo i love the little favicon next to the link for Salma😍
@colbyfayock
like adding the icon like that generally
@bepsays
There should be trailing WINDOWS style slashes ... :-)
@colinaut
Whoa that is a more complicated topic than I expected and I’ve been in this web game for a long time. Thanks for the detailed article!
@zachleat
Thanks! That’s using an Eleventy API Service zachleat.com/web/indieweb-a… more at 11ty.dev/docs/api-servi…
@zachleat
😅😅😅
@sebastienlorber
thanks for presenting nicely me research😄 happy to see people find this useful
@zachleat
Deeper deep dives as a service! You’re very welcome!
@macbraughton
@chrismunns
this was fascinating. thanks for sharing!
@zachleat
You’re very welcome Chris!
@graysonhicks
Interesting discussion on Gatsby adding integrated trailing slash behavior: github.com/gatsbyjs/gatsb…
@zachleat
Practically speaking, will that option swap from resource/index.html to resource.html?
@sebastienlorber
no, that's what I warned them about 😆 github.com/gatsbyjs/gatsb…
@zachleat
Nicely done, thank you! 🙌🏻
@ngriffin_uk
I think it’s good for UX personally. Doesn’t really make a difference app wise if you build for it.
@zachleat
did… did you read the blog post 😅
@ngriffin_uk
Oh in terms of deployment sure, if you use a third party.
@zachleat
I specifically go over a few different perspectives in the blog post, not limited to deployment 👀
@ngriffin_uk
Yeah I read it :). Was just saying that you can build around a lot of those issues. Redirect performance being one you sort of can’t, although I wouldn’t expect numbers to be high there. Google does this automatically per spec for root as well, so it just keeps it uniform. IMO.
@ckirknielsen
Love theses kinds of posts, thank you, Zach! So I have a weird situation: In Chrome, disable JS & load chriskirknielsen.com/fonts/ottseles… (no slash). It fails to load the relative assets, stays unslashed. Firefox adds the slash. Am I missing something? (11ty on Netlify, As… Truncated
@ckirknielsen
(not expecting you to troubleshoot for me, and I don't want to take up your time — I have a JS redirect in place so it's fine, it'll work!)
@zachleat
on super quick glance I’m guessing it’s this script on your page?
@ckirknielsen
Right, that's my "fix" after noticing the slashless URL didn't find the font/image files. It's a gross fix, but it works. 😅
@zachleat
ah right on. I’ll look at this warning on @slorber’s guide github.com/slorber/traili… about Post Processing—sounds like you have Pretty Urls disabled?
@zachleat
*I’d
@LiamBigelow
Another line in the table would be helpful: non-platform hosting. What happens when hosted as a static directory behind nginx/caddy/apache (Helpful in that it bolsters my trailing slash bias 👀)
@ckirknielsen
So I have tested: 1. Disable Asset Opt (D.O) checked: no slash (according to the guide, Pretty URLs (P.U) is still enabled). 2. D.O unchecked, P.U unchecked: no slash, broken CDN assets 3. D.O unchecked, P.U checked: slash, broken CDN assets I must be doing something wrong 😅
@jkc_codes
but what about the assault on our eyes when it comes to fragments? /page/#fragment 🤮
@zachleat
You want case 3, right? Canonical = slash? There might be another variable happening here to break your assets (with the slash)
@zachleat
/page/#:~:text=I%20can%E2%80%99t%20hear%20you
@marcfilleul
I saw you forked Sebastien's when starting my working day 12 hours ago and immediately checked my websites. My Netlify hosted ones were fine but I had to add cleanUrls and trailingSlash options to my Vercel hosted ones. I was also thinking a blog post would be great and tadaa… Truncated
@marcfilleul
And for the record, I voted with trailing slash in the poll and was sad to be wrong (regarding the most voted answer). But the repo and you post made my day 😅
@zachleat
Haha—oh no folks are watching my git history—*looks around* Haha *looks around* 😬😅
@marcfilleul
It was pure random as you were the first in my Github activity feed 😉. But it was perfect as I've been hooked by your poll.
@AntBogarin
seroundtable.com/google-trailin…
@networkaaron
Tip: When a 301 is not an option, on the client-side, have the URL in the address bar reflect the one in the canonical tag to help users share the preferred URL. “You cannot use a redirect rule to add or remove a trailing slash.” - docs.netlify.com/routing/redire…
@ashur
from one apologist to another, I really appreciated this post
Cacahuète et nougat
@nhoizey mince, c'est tombé en marche. Il doit y avoir un bug 😄
@danleatherman
You might like this post by @zachleat as well: zachleat.com/web/trailing-s…