Trailing Slashes on Netlify
December 29 2020
EDIT April 2021: While this guide may be useful, I've since published more refined work pertaining specifically to Gatsby.js that you may want to reach instead!
Alright, jokes aside, this is a great question. Aside from keeping your site concise, DRY, and friendly, the trailing-slash conundrum can impact your SEO... which can impact revenues. It's a big deal! For starters, let's quickly clarify that www.example.com/test and www.example.com/test/ are different URLs, and if the same content is rendered at both, you've got duplicate content. That's bad for SEO. Let's fix it.
Let me start by making something clear: in the general space of SEO ratings and web principles, it doesn't matter if we do or don't use a trailing slash for our site. The importance lies in choosing one approach and being consistent with it across all URLs within that site. Most folks don't care whether the site they're viewing the content of is using a trailing slash or not; they simply care that they found the article (either from a share or Google) and that it resolves into helpful content in their browser. Trailing-slashes are for SEO and analytics.
That said, don't fight your tooling. For reasons I'm about to dive into, Netlify is biased toward using the trailing-slash. If you're on board with this, unifying your site to always use the trailing slash approach is absolutely feasible. If you're on Netlify and don't want to use the trailing-slash, you're going to have a
bad difficult time.
As both a point of impetus and reference, this article came to be while working on this site. This is a Gatsby site and I couldn't ever quite figure out how to get the trailing / not-trailing slash working correctly, so I went down the rabbit hole. It goes deep. This article ought to provide you all you need to know for unifying your site's trailing-slash behavior, regardless of your site being built on Gatsby, Next.js, Nuxt, Jekyll, etc.
For starters, I want to break things down into two different perspectives. This will help clarify the mental model of what's-going-on-where.
/test/, web-crawlers are going to index your content on both URLs! That's duplication!
Luckily, Netlify offers a feature that will automatically redirect (directory) requests from
/test/! This is just one part of the puzzle, but we need to enable this feature so that our content will only ever be accessible on the trailing-slash variant. The feature is called "Pretty URLs" and can be found within your site's Build & Deploy settings, under Asset Optimization. The key here is to make sure that "Disable asset optimization" is un-checked and "Pretty URLs" is checked.
I believe these settings are cemented on the next deploy, so if you feel the need to verify, go ahead and deploy a new version of your app then pull out your favorite command-line HTTP request tool (mine's httpie, but feel free to use
cURL or anything else). Since we're specifically checking the server response behavior, there's no need to load anything in the browser yet. Here's what it looks for this site now that I've made that change.
When I hit the un-slashed URL, I get redirected to the slashed URL:
And subsequently when I request the slashed version from the get-go:
Great. So now Netlify's servers are forcing content to a single URL. That's all we need to do on the Netlify side! Let's dig into part 2).
Or perhaps 'the address bar can be deceiving'. See, once a webpage is loaded from the initial server request described above, it's the content of that page that shapes the user's next steps. If that page has an anchor
/blog (on a pure static site), clicking that link will force the user to hit the
/blog ➡ /blog/ redirection behavior we just setup above. That's an extra round-trip to the server for any link the user wants to click on. No good!
Unfortunately, this does mean that we (as the site developers) need to go through and update all of the
<a> anchors across the site and ensure that they're targeting a trailing-slash target... at least for links on our own site. This also includes any special internal-links that PWA-based SSGs use for versatile client-side routing:
<Link to="/test/" (Gatsby),
<Link href="/test/" (Next.js), and
<NuxtLink to="/test/" (Nuxt), for examples. This also includes any site-workflow buttons or links - things like pagination buttons and/or tags buttons etc. Don't forget any Markdown content you might have as well:
[my blog](/blog/) and/or any external sources (WordPress, GraphQL, Contentful, etc.) that may have rich-text / HTML content you serve. All of these anchors need to be updated to the trailing-slash target. Even if you're okay with your users needlessly hitting the server for an additional roundtrip just to be told to add a slash, these updated anchor target values get embedded into the json
page-data that the PWA relies on once it's been hydrated client-side, and that data not containing the trailing-slash variant of your anchor targets can cause problems.
Finally, we need to make sure that our SSG's generated pages are also converted to 'trailing-slash format'. In this case, that means that our SSG is generating
index.html pages within a named folder rather than simply a named html page. This calls back to historical web parlance, where the path in a URL implied a directory if it ended with a slash and implied a document if it did not.
The idea is this: if you have a document,
/blog/yay-its-summer.html (remember that
summer.html is just a document like any other), Netlify's Pretty URLs feature that we enabled in part 1) will prettify that path to
/blog/yay-its-summer... note the distinct lack of trailing slash there. That's because the path specifies a document, not a directory. While a great feature, Pretty URLs respects web parlance 😅. The fix is to convert the document to a directory. In this case, changing
/blog/yay-its-summer/index.html. Now Pretty URLs will prettify that path to
/blog/yay-its-summer/! That's what we want! Pretty URLs still adheres to document vs. directory parlance, so we need to adjust our SSGs to generate content according to that parlance as well.
There isn't exactly cut-and-dry advise on how to get your SSG running in this way, though. For instance, Gatsby generates pages automatically for any file under
./src/pages/, but Gatsby is also widely built around the
createPages() API - meaning that pages can be built both from Gatsby's automatic page-detection and your theme's implementation of
createPages(). The open-source plugin gatsby-plugin-force-trailing-slashes may provide the functionality for nesting all resulting documents inside of directories (make sure you put it last in your
plugins array), but you'll want to take special care to ensure that any pages created by the
createPages() API are converted to
index.htmls nested within directories. Remember that you can always run
gatsby build locally and inspect the resulting
On the other hand, Next.js and Nuxt both have config values that can enable and force trailing slashes on all routes. Neat! I'll bet money it's doing the same directory-nesting I just described... Nuxt even warns that all internal links need to be updated to the trailing-slash target, so thank goodness we already did that 😉
For the non-PWA SSG users out there, Jekyll's routing configuration can help you out, but simply reconfiguring your site's file structure can often make the difference.
Once you've figured out how to correctly nest your SSG's resulting documents within directories...
With your documents correctly converted to directories, all of your anchor targets updated, and Netlify's Pretty URLs feature activated for your site, you can now confirm that all of your content is uniquely accessible from one (non-duplicated) trailing-slash URL, and that all of your content adheres to the trailing-slash system! Do some checks (both on command line and the browser) to make sure all of your links look like they're going to the right places, double check that you didn't miss any fancy PWA internal-navigation-Links, and rejoice! You've now conquered the trailing-slash-conundrum. Huzzah.
P.S. Did I miss something? Still having trouble? Comment below and we can work it out!