Zach’s ugly mug (his face)

Zach Leatherman

Trailing Slashes on URLs: Contentious or Settled?

January 24, 2022 #1 Popular 18,844 Views

After some discussion with IndieWeb Avatar for https://whitep4nth3r.com/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:

  1. 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.
  2. 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.
  3. 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.
  4. 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:

  1. Developers, wanting to implement a personal or team preference.
  2. App/site/framework tooling (e.g. say, uh, you’re the maintainer of Eleventy)
  3. 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.

IndieWeb Avatar for https://sebastienlorber.com/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.
  • /resource/
    • ✅ All hosts agree that /resource/ should return content from resource/index.html
  • 💔 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 from resource.html
    • 🆘 Warning: Confusingly Vercel is the only host tested that returns a HTTP 404 error for /resource.
  • /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.

⚠️ 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 from resource.html
  • /resource/
    • ✅ Almost everyone agrees that /resource/ should return content from resource/index.html
    • 🆘 Warning: Netlify redirects to /resource instead.

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.

IndieWeb Avatar for https://www.11ty.dev/Eleventy Specific Note

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.htmlresource/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.

Zach’s ugly mug (his face)

Zach is a builder for the web with IndieWeb Avatar for https://www.netlify.comNetlify. He created the IndieWeb Avatar for https://www.11ty.devEleventy site generator and is still fixated on web fonts. His public speaking résumé includes talks in eight different countries at events like Jamstack Conf, Beyond Tellerrand, Smashing Conference, CSSConf, and The White House. He is an emeritus of Filament Group, NEJS CONF, and still helps out with nebraskajs’s AvatarNebraskaJS. Read more about Zach »

Previous
Back to the Facepile, Featherweight Edition
Next
CSS-only External Link Favicons

86 Retweets

Chris Ferdinandi ⚓️Matt BiilmannSebastien Lorber 🇫🇷 🦖 ⚛️ 📨⚡️ Salma | whitep4nth3rjalbertbowdeniiChris MunnsmyfFynn BeckerTim SeverienCharles BauerBasix잇창명 EatChangmyeong💕Sudeep BiswasJohn Kemp-CruzAlistair ShepherdBen Myers 🦖Oliver KohlBarry PollardJoseph ScottEric HoweyFelipe BarbosaadnanAnders RingqvistIndieWeb Avatar for https://www.alvinashcraft.comShobhit SharmaDan BurzoHystThomas SteinerSimon PlenderleithDave Harrison/𝔡𝔢𝔱/𝔩𝔢𝔣𝔣 ᛑᛂᛐᛛᛂᚡVladimir SimovićKevin PowellFarhad AzarbarzinNiazJani KarhunenFrontend Daily 🚀HN Front PageHacker NewsWinson TangFabian VilersPinboard PopularSarthakAngsuman ChakrabortyRob RoyIndieWeb Avatar for https://rssfeeds.cloudsite.buildersJosué AcevedoJackyShobhit SharmaPhilippkimizuyJames Sinclairadpegu 🏄IndieWeb Avatar for https://melanie-richards.comIndieWeb Avatar for https://onlineeverday.roIndieWeb Avatar for https://vcodepedia.comIndieWeb Avatar for https://m-softtech.inIndieWeb Avatar for https://shila.praker.inIndieWeb Avatar for https://www.gictafrica.comIndieWeb Avatar for https://gadgetofficials.comIndieWeb Avatar for https://hapidzfadli.idIndieWeb Avatar for https://tutorials.blog.crabberspost.comIndieWeb Avatar for https://aayugcreation.comIndieWeb Avatar for https://apoirier.dectim.caIndieWeb Avatar for https://zephyrnet.comIndieWeb Avatar for https://brotherstore.onlineIndieWeb Avatar for https://www.nolisa.xyzIndieWeb Avatar for https://www.67nj.orgIndieWeb Avatar for https://datechguru.comIndieWeb Avatar for https://brinynews.comIndieWeb Avatar for https://hnikoloski.comIndieWeb Avatar for https://opta.liveIndieWeb Avatar for https://tgrafix.co.zaIndieWeb Avatar for http://usae.siteIndieWeb Avatar for http://ecapital.newsIndieWeb Avatar for https://css-tricks.comIndieWeb Avatar for https://livingogroup.comIndieWeb Avatar for http://icapital.newsIndieWeb Avatar for http://e-capitals.comIndieWeb Avatar for https://codytechs.comIndieWeb Avatar for http://i-capitals.comIndieWeb Avatar for https://underskore.inIndieWeb Avatar for https://sheriengineering.comIndieWeb Avatar for https://hapidzfadli.idIndieWeb Avatar for https://codinghero.live全部入りHTML太郎

112 Likes

Marty McGuireJose Ma-boyrezzedup Andrew ChouNikrooz 👨🏻‍💻Tomasz CzajęckiBryce WrayShawn SearcyDavid EastFarhad AzarbarzinNiazGustavo sem OmilocasagrandeDaniel HerzogVladimir SimovićSimon PlenderleithOscar Pagani/𝔡𝔢𝔱/𝔩𝔢𝔣𝔣 ᛑᛂᛐᛛᛂᚡKaiHystNothing Personal, but #dirumahajaPaolo BarboliniAnders RingqvistBjörn RixmanNachiketrazhFelipe BarbosaEric HoweyRyan MulliganEric ☕️ 🐟 💻Lucid00Barry PollardJeremie ChauvelAdrian ThomasLewis Cowper 💉💉💉Oren Elbaumdominik kundel 🐼 💉💉 💉Jeremy Wynntommy georgeDaeron StormblessedAxel RauschmayerMatthew ChwatThreadPatrick HaugDez BebernissAlistair ShepherdJames LohJess Peck 🐍🤖Benjamin GriesMarc Filleul 🇫🇷Matt KanebebsicoDevessierJens GeilingJoe GaffeyDavid Hund ✌ArpitRyan CormackPatron saint of baby carrotsSudeep BiswasZhenghao HeFarai GandiyaJody DonettiSvenScott MathsonLaraKudo MonstroJohn Kemp-CruzSidKristofer KoishigawaMatt BiilmannFynn BeckerChris CoyierMaxime RichardMark BuskbjergCharles BauerMatt Tunney ☕️Peter ÇoopèrAdrian Bece (アドリアン・ベツエ)Grayson HicksBrooke Chalmers 🏳️‍⚧️Colin FahrionTravis Waith-Mair - Bedrock Layout GeniusDonnieFrançois Best 🛠️JiabinTheo - t3.ggJustin Time, Stanley.Mark GohoTowhidMarc Littlemore 👋🏻Bart van de Biezenhenry ✷Joshua YoesjalbertbowdeniiAdam LuptakChris MunnsLee FisherMatt Hobbs v2.0 💉💉💉Will 🛸Vipul 🌻Matt Rossman 🍌Krzysztof JeziornyZac SkalkoGaël PoupardLynn FisherS#Wade WegnerJames RossStephen CunliffeEric WallaceNathan SmithWill Boyd

1 Bookmark

IndieWeb Avatar for https://nicolas-hoizey.com
42 Comments
  1. 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!)

  2. Zach Leatherman

    @zachleat

    ThIS POSt WOUlD noT HaVe BEEn POSSiblE WiThout tHiS EXcELlenT resEARch fRoM @SeBASTIENLORBEr GITHub.Com/SloRBER/tRaILI…

  3. Zach Leatherman

    @zachleat

    Completely agree!

  4. Colby Fayock

    @colbyfayock

    oooo i love the little favicon next to the link for Salma😍

  5. Colby Fayock

    @colbyfayock

    like adding the icon like that generally

  6. Bjørn Erik Pedersen

    @bepsays

    there shOuLD be TRAiLInG windoWs STyLE slAshES ... :-)

  7. Colin Fahrion

    @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!

  8. Zach Leatherman

    @zachleat

    Thanks! That’s using an Eleventy API Service zachleat.com/web/indieweb-a… more at 11ty.dev/docs/api-servi…

  9. Zach Leatherman

    @zachleat

    😅😅😅

  10. Sebastien Lorber 🇫🇷 🦖 ⚛️ 📨

    @sebastienlorber

    thanks for presenting nicely me research😄 happy to see people find this useful

  11. Zach Leatherman

    @zachleat

    Deeper deep dives as a service! You’re very welcome!

  12. Donnie

    @macbraughton

  13. Chris Munns

    @chrismunns

    this was fascinating. thanks for sharing!

  14. Zach Leatherman

    @zachleat

    You’re very welcome Chris!

  15. Grayson Hicks

    @graysonhicks

    Interesting discussion on Gatsby adding integrated trailing slash behavior: github.com/gatsbyjs/gatsb…

  16. Zach Leatherman

    @zachleat

    Practically speaking, will that option swap from resource/index.html to resource.html?

  17. Sebastien Lorber 🇫🇷 🦖 ⚛️ 📨

    @sebastienlorber

    no, that's what I warned them about 😆 github.com/gatsbyjs/gatsb…

  18. Zach Leatherman

    @zachleat

    Nicely done, thank you! 🙌🏻

  19. Nicholas Griffin

    @ngriffin_uk

    I think it’s good for UX personally. Doesn’t really make a difference app wise if you build for it.

  20. Zach Leatherman

    @zachleat

    did… did you read the blog post 😅

  21. Nicholas Griffin

    @ngriffin_uk

    Oh in terms of deployment sure, if you use a third party.

  22. Zach Leatherman

    @zachleat

    I specifically go over a few different perspectives in the blog post, not limited to deployment 👀

  23. Nicholas Griffin

    @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.

  24. Christopher Kirk-Nielsen

    @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

  25. Christopher Kirk-Nielsen

    @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!)

  26. Zach Leatherman

    @zachleat

    on super quick glance I’m guessing it’s this script on your page?

  27. Christopher Kirk-Nielsen

    @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. 😅

  28. Zach Leatherman

    @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?

  29. Zach Leatherman

    @zachleat

    *I’d

  30. Liam Bigelow

    @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 👀)

  31. Christopher Kirk-Nielsen

    @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 😅

  32. John Kemp-Cruz

    @jkc_codes

    buT wHat ABOut thE ASSaULT ON OUR eyes wHEN iT CoMEs to FRAGmENts? /PAGE/#fRagMenT 🤮

  33. Zach Leatherman

    @zachleat

    You want case 3, right? Canonical = slash? There might be another variable happening here to break your assets (with the slash)

  34. Zach Leatherman

    @zachleat

    /page/#:~:text=I%20can%E2%80%99t%20hear%20you

  35. Marc Filleul 🇫🇷

    @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 tadaaa 😊

  36. Marc Filleul 🇫🇷

    @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 😅

  37. Zach Leatherman

    @zachleat

    Haha—oh no folks are watching my git history—*looks around* Haha *looks around* 😬😅

  38. Marc Filleul 🇫🇷

    @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.

  39. ant

    @AntBogarin

    seroundtable.com/google-trailin…

  40. Aaron K.

    @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…

  41. from one apologist to another, I really appreciated this post

  42. Cacahuète et nougat

    Cacahuète et nougat

    @nhoizey mince, c'est tombé en marche. Il doit y avoir un bug 😄

    Shamelessly plug your related post:

    Sharing on social media?

    This is what will show up when you share this post on Social Media:

    How did you do this? I automated my Open Graph images. (Peer behind the curtain at the test page)