Trailing Slashes and Gatsby

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 createPage() API.

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 createPage() directly from our own javascript and explicitly tell Gatsby what the ‘intended HTML page path’ should be.

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 path of "/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 path of "/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 createPage().

*:The only exception to this rule is if you pass a path that explicitly already ends with .html to 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 createPage() a 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 createPage() like "/blog/post-5" or "/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 cd you’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 <Link>s, 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), you’ll get a 301 to the slashed version. But if you’re running Javascript right now, this link CLICK ME will actually take you to /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 <Link>s, 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" to 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 <Link>s and 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 createPage()? That path string also helps configure the top-level Gatsby (Reach) Router under the hood. I’m not entirely sure where yet (the Gatsby repository is massive), but when the top-level Router gets configured during the site build, the path it receives for a page drives what the Router will do once it’s spun up in the Javascript on that page.

Here’s what I mean. Take my site’s Blog index. Say that we pass "/blog" to createPage() — we know from above that it’ll ultimately print the HTML to ~/public/blog/index.html, but what happens when we load jonsully.net/blog/ in the browser? Well, if you do it with Javascript disabled, it works just fine! But if you enable Javascript you’ll see that as soon as you load the page, the trailing slash suddenly falls off! And if you refresh your browser, you’ll see the trailing slash pop in for a moment then quickly fall off again! What the heck!

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 /blog/ (with slash) because that’s what the actual file is. Once that page loads in your browser though (once the Router loads in Javascript), it’s reverting the address bar back to the non-slash version of your path because that’s the path it was configured with. The 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 /blog, not /blog/, so once the Router spins up in Javascript on your browser, it drops the trailing slash!

That’s particularly unhelpful because when you refresh the page, it requests the non-trailing-slash version of the page from the server (that’s what’s currently in your address bar, after-all) but the server does its job and pushes you to the trailing slash version. Your browsers follows and receives the content for the trailing slash version and… as soon as the Router spins up in Javascript, it drops the slash again! Annoying, right?!

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 path strings have a trailing slash correctly primes the top-level Reach Router to expect the resulting page to have a trailing slash once it spins up in Javascript — it ensures that the URL served by the server matches the URL expected from the Reach Router. The good news is, you don’t have to go through all of your calls to createPage() in 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:

  1. Leave “Pretty URLs” enabled in Netlify (everyone should pretty much always)
  2. Ensure that when I write <Link>s or programmatically navigate(), I include a trailing slash on that link target
  3. Use gatsby-plugin-force-trailing-slashes

That’s it!

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!

Photo by @jacobresch on Unsplash

Want to stay in the loop?

1 - 3 emails/month. No spam.
No data selling. Never.

Join the Conversation

Latest Blog Posts

New Gear Day 😁

New Gear Day 😁

Just another S(tr)aturday

July 17 2021

Jon's Velvia (X100V Film Simulation)

Jon's Velvia (X100V Film Simulation)

FXW's Rockwell Velvia with tweaks

July 15 2021

Life Update July 2021

Life Update July 2021

Just a little update on things 🙂

July 12 2021

Going Deep: Follow-Up Pt. 1

Going Deep: Follow-Up Pt. 1

5-6 months in, how's the journey been?

May 24 2021