Trailing Slashes and Gatsby
14 min read | April 16 2021
I’ve previously written about trailing slashes and how they work with regard to Netlify as a site host, but since I primarily use Gatsby for my static or mostly-static projects and get plenty of questions around configuring that properly, I wanted to carve out the space to write how I configure my projects for their trailing-slash success.
I use trailing slashes for all of my Gatsby projects and I recommend that for others, too. Let me explain the ‘why’ behind the stance. Two main points here.
1) For starters, Gatsby implies that we should use trailing slashes by its automatic directory-ization of pages when calling the
When you have pages that are built ‘automatically’ by virtue of defining a
.js file in the default
~/src/pages/* folder, Gatsby does the lifting for you — it calls the
createPage() method under the hood and passes in a desired page path that matches the file path relative to the
~/src/pages folder. In context, that looks like the following: if you have a file like
~/src/pages/jon.js, Gatsby will ultimately call
createPage() with that file’s React component and pass the desired path of
"/jon". If you have a file like
~/src/pages/blog-posts/number-five.js, Gatsby will ultimately call
createPage() with that file’s React component and pass the desired path of
"/blog-posts/number-five". Pretty straightforward — this is one of the nicer features of Gatsby: add a file (or folders containing files) to the
~/src/pages directory and they’ll ‘automatically’ become pages. The key note here is the
path that gets passed into the
createPages() function. I’d call this the ‘intended HTML page path’.
On the programmatic page-building side, we call
createPage() directly. This is typically within our
gatsby-node.js file as a sub-method of the
createPages() exported-function. In these cases we call
Be it programmatic or automatic, either way
createPage() receives a
path argument (a string) that informs Gatsby on how / where to build the resulting HTML.
The reason I say that Gatsby implies we should use trailing slashes is because Gatsby directory-izes
paths you give to
createPage(). Directory-izes isn’t a word, but I digress. When you pass in a
"/blog-posts/number-five" (as noted above, an ‘automatic’ path), Gatsby actually takes that path and modifies the “print-the-HTML-here” path to
/blog-posts/number-five/index.html. This is true for ‘automatic’ pages and programmatically-created pages. If you programmatically call
createPage() and pass a
"/cars/mustang/", the trailing slash on that
path string is not what causes the resulting HTML file to be an
index.html contained within a folder named
mustang, Gatsby does this directory-ization with any*
path you pass in. So every page you create with Gatsby is going to be directory-ized. This is also regardless of where you source your data from in the
gatsby-node.js file. It may come from the filesystem, Contentful, Shopify — it doesn’t much matter because the directory-ization occurs after you get the data and pass it into
*:The only exception to this rule is if you pass a
path that explicitly already ends with
createPage(). This will never be the case with automatically-created pages, but you could make this work with the programmatic calls to
createPage() in your
gatsby-node.js file. I haven’t tried it myself, but the code does show that if you give
path string that already ends in
.html, it won’t directory-ize the page and will instead print the resulting HTML code into that file name. This is really the only way to have Gatsby produce documents instead of directories. (*Gatsby does also generate a document instead of a directory for the 404 page [printed to
~/public/404.html] and the site root/index but those are the only exceptions I know of, and they’re hard-coded in Gatsby’s source)
Given that the very normal, default case is to pass
path strings to
"/info/about-me/" and that Gatsby will directory-ize both of those, I’m comfortable making the statement that Gatsby does imply that we should use trailing slashes (recall that in traditional web-server parlance, viewing a directory index == a trailing slash and viewing a single document’s contents == not a trailing slash). But now you at least know the secret to having Gatsby print documents too! Use it wisely 😆
2) Perhaps more of an implementation detail, but the second reason I use trailing slashes on all of my Gatsby sites is because I host on Netlify. Netlify by default, and really in the only way I recommend to folks to use it, follows standard web-server trailing-slash parlance. It renders a file like
/blog/post-1/index.html on the URL
/blog/post-1/ and redirects requests to
/blog/post-1 (no slash) to
/blog/post-1/ (slash), accurately reflecting that the document is actually an
index.html document contained within the named ‘post-1’ directory. Similarly, if you have an actual document, say,
/blog/my-next-post.html, Netlify will render that file at path
/blog/my-next-post (no slash) and redirect requests from
/blog/my-next-post/ (with slash) to
blog/my-next-post (no slash), accurately reflecting that the document is indeed a document, not an
index.html contained within a named directory.
This feature is called “Pretty URLs” and while its interface for turning it off and on is very, very unintuitive, it’s on by default and should not be turned off. Turning it off means that your content will be served on more than one path and that’s bad for SEO.
Put shortly, Netlify’s hosting for HTML files follows the standard web-server framework that directories with index files are served as trailing slashes and documents are served without the slash. I think most static-file hosts do this, as it’s been the default for web-servers for decades, but YMMV beyond Netlify.
So, with Gatsby making directory-ized
index.htmls for us by default given any reasonable
path string and Netlify by default serving named-directory-
index.htmls in the traditional, trailing-slash, way… my premise here is simply that I don’t want to fight my tooling. 😅 I use trailing slashes.
Can you not use trailing slashes with Gatsby? Yes, it’s doable. It’s just a few more hoops to go through. You don’t need to change anything on the Netlify side (again, leave Pretty URLs on), but you will need to change your
gatsby-node.js file and you can’t use the ‘automatic’ page generation that results from adding files to
~/src/pages/*. If you have pages there, you’ll need to remove them and generate them via the
gatsby-node.js file instead. The trick is (as noted above), any time you call
createPage() in your
gatsby-node.js file, you need to make sure the
path string doesn’t end in a
/ then explicitly add
.html to the end. For example, the page you’re reading now is written in Markdown and my Gatsby site uses the well-known Gatsby Remark plugin to source the markdown content in my
gatsby-node.js’s GraphQL query. Given my setup, the
path I pass to
createPage() looks like (for this page)
"/blog/trailing-slashes-and-gatsby/". If I wanted to convert that to result in a document rather than a directory, I’d need to adjust that
path string to
"/blog/trailing-slashes-and-gatsby.html" before I give it to
createPage(). Only then will my blog posts generate as documents rather than directories, where Netlify will serve them without trailing slashes.
Please do understand, changing the trailing-slash strategy on a site that’s already been on the web for a not-insignificant amount of time can be very dangerous. Again, using this site as an example, if you request
jonsully.net/about you’re going to get a 301 Permanent Redirect to
jonsully.net/about/. That’s good because it means that my “about page” content is only ever accessible at one URL (the trailing-slash one). But if tomorrow I change my trailing-slash strategy to instead generate documents and intend for my users to see my content at
/about rather than
/about/, folks that have visited that page before are going to have a problem. The 301 they got yesterday is permanently cached in their browser. Their browser knows that if it sees a request for
jonsully.net/about, it shouldn’t even fire off a request to the server at the address, but instead remember the 301 it received X years ago and instead go right to
jonsully.net/about/. Well, now that I changed my strategy, requesting
/about/ is actually going to 301 back to
/about. See the infinite loop forming? Not good. Once you pick a strategy, stick with it. If you aren’t sure which one you’re using or which one you want to use, move very slowly and be very careful. The permanence of 301s can be tough to swallow if you’re on the wrong side of it.
So is that it? Nope! That covers the server / static-files side of Gatsby. There’s a whole ‘nother layer! When a Gatsby page is loaded into a user’s browser and React re-hydrates, the Gatsby site magically turns into a single-page-app! It’s a neat feature and one I’m very fond of, but it does require some extra carefulness when it comes to trailing slashes.
The reason we need to be extra careful when it comes to the client-side routes is because Gatsby’s client-side router is the
@reach/router. The @reach/router doesn’t care about trailing slashes. As noted here:
The difference from the command line here is that you can’t be “at a file” in the command line. So any time you
cdyou’re working relative to directories only. On the web you can be “at a file”, so the trailing slash indicates if you’re “at a file” or “in a directory”.
Reach Router made a choice to ignore trailing slashes completely, therefore eliminating any difference when navigating up the path.
Where that can bite us with Gatsby is in our
navigate()s and if we’re using client-side-only routes via another instance of the Reach Router. Here’s how this plays out. My ‘About’ page is served exclusively on
/about/ (with slash) from the server. If you use a command line to cURL
/about (no slash) and render it just fine. That’s because that link is a SPA link powered by the Reach Router. Once a Gatsby site’s React hydrates on the first page the user loads, the rest of the navigations throughout the site should be from client-side routing and client-side navigations a la SPA.
I prefer my URLs to be uniform. Both for the sake of having a clean look but moreso in case someone shares a URL from my site by copying their browser’s address bar. I don’t want the recipient of that share to hit the link and wait twice as long while a 301 processes! I want them to see the content as soon as possible! So I take the (not-my-favorite) extra step of refactoring all of my
navigate()s, and extra routers to use trailing slashes. That means anywhere I directly use a
<Link> component, I checked or changed the
to= from (example)
to="/about/" so as to force the Reach Router to render the trailing-slash in the address bar. Same with
navigate()s. And same for links in Markdown if you’re using gatsby-plugin-catch-links (which you should!!).
You can go through similar steps to force trailing slashes for any client-side-only routes you’ve made by pulling in the Reach Router for yourself, but the benefit may be marginal since those routes won’t be scanned for SEO / etc. by definition (client-side-only).
That “CLICK ME” link above is the one place on this site I’m leaving a link trailing-slash-less for the sake of example. If you follow the link, you should arrive on my ‘About’ page without a trailing slash in your browser address bar. Refresh the page and you’ll see it does indeed convert back to the trailing-slash variant since the server doesn’t actually serve anything on the non-trailing-slash variant — the only way we ever got to it in the first place is because the Reach Router didn’t care (and didn’t know to care).
Following up for the non-trailing-slash folks, if you decided to go without trailing slashes after our discussion of the
gatsby-node.js rendering process and web-server standards, you have a similar responsibility: make sure all of your
navigate()s etc explicitly don’t use a trailing slash. Same problem just vice versa 🙂
Beyond that, there’s one last step. Remember toward the beginning when we were talking about the
path string that gets passed to
createPage() that drives exactly where Gatsby prints the HTML that results from the component passed to
Here’s what I mean. Take my site’s Blog index. Say that we pass
createPage() — we know from above that it’ll ultimately print the HTML to
~/public/blog/index.html, but what happens when we load
What’s going on is that Gatsby printed your file to a directory index because the
path string you gave for that page didn’t end in
.html — so the server is correctly pushing you to
path string you give to
createPage() is the same
path string that the Reach Router gets. Even though
createPage() doesn’t care if it has a trailing slash or not (it’ll directory-ize either way), the Reach Router configuration does. It was configured with
The answer here is to make sure that the
path strings you give to
createPage() all end in a trailing slash. While it makes no difference for the directory-ization process of the produced HTML files, ensuring that those
gatsby-node.js — just install gatsby-plugin-force-trailing-slashes and it will automatically ensure that the
path string given to any call of
createPage() ends in a trailing slash. Yay!
So, to recap: I use trailing slashes for all of my Gatsby sites. I just think it’s easier and better that way. To accomplish this, I:
- Leave “Pretty URLs” enabled in Netlify (everyone should pretty much always)
- Ensure that when I write
<Link>s or programmatically
navigate(), I include a trailing slash on that link target
- Use gatsby-plugin-force-trailing-slashes
I realize this is a very long post with a very simple 3 step “how to” but I think the understanding behind the 3 steps is critically important. Hope that helps!