Trailing Slash for Frameworks

9 September 2024 (Updated 13 September 2024)

It turns out that putting a / at the end of a URL can be a big deal. Should you put it? Or should you not? Or should you simply make both work? Why is this even a problem in the first place?!

This guide unravels the concepts and intricacies of all that, with a focus for framework maintainers and library authors who want to understand trailing slash better and implement a consistent experience for everyone.

If you're a developer, an end-user, or just curious, you'll see what goes behind the scenes and understand better why it matters!

Table of Contents

Which is better?

There's 4 ways to handle trailing slashes, but is there a superior option?

  1. Disallow trailing slashes. You cannot access a path with a trailing slash.
  2. Enforce trailing slashes. You cannot access a path without a trailing slash (unless the path is the root or points to a resource directly, like example.com/data.json).
  3. Allow either trailing slash or no trailing slash on per-route basis. For example, if you're using a framework and define a path at src/routes/file.jsx, then it can only be served at /file and not /file/. Vice-versa, if you define a path at src/routes/folder/index.jsx, then it can only be served at /folder/ and not /folder. Or if the framework allows explicitly defining a kind of trailingSlash option for a route.
  4. Allow both trailing slashes and no trailing slashes, which would serve the same content.

When you cannot access a path, it doesn't mean that it'll return a 404 not found. It could also return a 301 redirect to the correct path and doesn't degrade the user experience. It'll depend on how the server is configured.

Among these 4 options, all of them are equally good except for no4. Choosing either no1, no2, or no3 is a matter of preference, and frameworks could choose either as the default as long as the file output path or request handling are correct, which we'll discuss later.

If you're a framework author who would not like to have a trailingSlash option, no3 is perfect as you can use the directory structure to depict the trailing slash behaviour. If you'd like to put less emphasis on the directory structure, a trailingSlash: 'never' | 'always' option would be needed to make things explicit, whether it's applied to all routes by default (no1 or no2) or on a per-route basis (no3).

The reason why no4 isn't recommended is because a path with or without a trailing slash is considered as two distinct paths, even if they have the same content. The downsides include:

  1. Search engines may be confused of which is the main URL to index.
  2. Users may access the same content from different paths, which could cause confusion when sharing links.
  3. Extra work needed to ensure relative references are correct.
  4. The HTTP request may be cached seperately, leading to duplicated work.

While no1 and no3 can be fixed with a <link rel="canonical"> tag or a <base> tag. It's a lot more work to get right, and we could avoid the downsides in the first place by performing a 301 redirect to the preferred main URL. But with that said, it's still fair for frameworks to support this option as long as it's done right and users are informed of the caveats.

Getting it right

Origins

HIstorically, trailing slash comes from the need and a set of rules that make URLs pretty:

Pretty URLFull URL
example.comexample.com/index.html
example.com/example.com/index.html
example.com/fileexample.com/file.html
example.com/folder/example.com/folder/index.html
example.com/folder/stuffexample.com/folder/stuff.html

It's shorter, cleaner, and much more memorable. It's a simple rule that servers can use to automatically check for index.html or .html when you're accessing a path.

As you may notice, the directory structure of the full URLs depict the pretty URLs and whether it requires a trailing slash or not, similar to option no3 as discussed in the Which is better? section.

The basis of this mapping is important to later understand how links are resolved, especially for relative references. Even though servers today may not have the actual HTML file physically on the filesystem (e.g. server-rendered pages), it's easier to reason about certain behaviours when you treat it as if it were. And of course futureproofing yourself if you decide to prerender the page later on.

Relative references

The most important effect of trailing slashes is how it affects relative references being resolved. The table from the previous section shows how it's able to prettify URLs without affecting relative references. Let's use this file tree to consider some cases:

├── index.html
├── file.html
├── folder
│   ├── index.html
│   └── stuff.html

Case A:

  1. /folder/index.html would like to reference /folder/stuff.html, it would use ./stuff.html.
  2. Given example.com/folder/, ./stuff.html will resolve to example.com/folder/stuff.html. ✅
  3. It should not drop the trailing slash like example.com/folder because the relative path would resolve to example.com/stuff.html, which does not exist. ❌

Case B:

  1. /file.html would like to reference /folder/stuff.html, it would use ./folder/stuff.html.
  2. Given example.com/file, ./folder/stuff.html will resolve to example.com/folder/stuff.html. ✅
  3. It should not have the trailing slash like example.com/file/ because the relative path would resolve to example.com/file/folder/stuff.html, which does not exist. ❌

While the example cases above show relative HTML references, the same applies to CSS, images, and other assets. And they don't always appear as <a> or <link> tags, some may be defined in JavaScript and it becomes tricky to fix them unless you have full control over the code.

As such, it is important to make sure file servers and server-rendered pages respect the significance of the trailing slash for relative references to resolve correctly.

Counterpoints

1. Writing an HTML file in anticipation of being served on a different URL

Let's say you have a /folder/index.html file, but the relative references within it are written in anticipation that the file will be served at example.com/folder instead of example.com/folder/. Technically this won't cause any problems because the relative links will point to the right paths.

However, in practice this makes reading the HTML file harder to reason about as you can't rely on the physical filesystem path to infer the URL path. Not all servers will be able to support this case unless there's an option to opt-in.

If you wanted to serve from example.com/folder in the first place, you should rename the file as /folder.html. A server that allows you to serve from a different path is more of a band-aid than fixing the source of the problem.

2. No relative paths

If you know that there's no relative paths in your app, then technically relative references shouldn't be a problem at all. However, it's usually not possible to guarantee that especially for user-written content, and limits your framework from supporting relative base: a feature that allows users to mount their server to any base of a URL, making it portable.

Support both trailing slash and no trailing slash

If you (as a framework or library author) decide to make accessing a path with or without a trailing slash to both work, here's some things to be aware of:

  1. Relative references (as mentioned in the previous section) should apply to both paths. Meaning you may have to generate different relative links when rendering both paths.
  2. If you prerender the page, you'd have to prerender it twice as two different files to make accessing the content with or without a trailing slash to both work.
  3. You should encourage users to select the preferred path using <link rel="canonical"> tag to indicate the main URL to index.
  4. Inform of the effects of duplicated HTTP caching due to the different paths.
Counterpoints

1. The <base> tag

With the <base> tag, theoretically you could only need to render the page once, and serve it at both paths with or without a trailing slash. For example with /folder/index.html, you can serve it at example.com/folder and use <base href="/folder/"> to resolve the relative references correctly.

However, this has the assumption that the content isn't derived from the current request URL. For example, if the user has access to the request URL (example.com/folder or example.com/folder/) and returns different content based on it, then you'd have to treat them as separate paths in order to render it correctly. So it may not always work without accepting the caveats.

2. Not HTML content

If the routes are not HTML content, but instead APIs like JSON data, then technically the above doesn't apply to you, except no4. You could be hitting cases where your API is being cached twice depending on the trailing slash, which may not be ideal for your server or end-users.

However, if the API route is example.com/api.json, it's usually not needed to append a trailing slash due to the extension, so in this case you can treat as only supporting the non-trailing slash path.

Redirects

If you decide to enforce trailing slashes or disallow them, instead of returning a 404 not found for the incorrect path, you can consider returning a 301 redirect to the correct path.

This way if you have links to the incorrect path, or if the user manually types the link, they'll get a better user experience being redirected to the correct path, and also allows search engines to index the right page.

Development and production consistency

Last but not least, make sure that any trailing slash behaviour and configuration applies to both development and production environments.

For example, if a page will be prerendered in production, but dynamically rendered in development, you should ensure that the URL the users can access from is the same as in production. If you can access localhost:3000/folder in development, but only example.com/folder/ in production, users will be bound to hitting relative references issues.

Ecosystem review

The section below takes data from https://github.com/slorber/trailing-slash-guide and expands on certain points from the discussion above. (Give the repo a star!)

Hosting providers

Dimmed cells indicate that the path may lead to relative references issues or they return an unexpected content.

As a framework, ideally it should able to support all of the hosting providers, which is possible but with many edge cases to keep in mind. Note that the data above focuses on static file serving only.

  1. Cloudflare Pages provides the best default that serves all files correctly with the prettiest URLs, and doesn't expose the relative references issues.
  2. GitHub Pages is a close follow-up, but doesn't serve with the prettiest URLs.
  3. Netlify (Pretty Urls on) is able to serve all correctly except for /both/, which should have rendered the /both/index.html file.
  4. Netlify (Pretty Urls off), Render, and Azure Static Web Apps serves all files very leniently and exposes risks to relative references issues if accessed from the incorrect path. Usually these are only ideal if the relative references in your HTML files are incorrect in the first place, or if it didn't matter.
  5. Vercel with any configuration seem to not get any generally right.
    • In all cases, both/index.html and both.html are very finicky, they don't exactly serve the right file that you expect.
    • With cleanUrls=true, it's more likely to clean some paths wrongly. It depends on the trailingSlash option instead of the actual file existence.
    • With trailingSlash=undefined, it exposes risks to relative references issues if accessed from the incorrect path.
    • With trailingSlash=false, only file.html variants will tend to serve correctly, other variants may not.
    • With trailingSlash=true, only folder/index.html variants will serve correctly, other variants may not
  6. If we ignore the both tests since they're rare, then:
    • Cloudflare Pages is still the best.
    • GitHub Pages, Netlify (Pretty Urls on), and Vercel (cleanUrls=false,trailingSlash=true) are a close follow-up, but they don't serve the prettiest URLs.
    • Netlify (Pretty Urls off), Render, Azure Static Web Apps, and Vercel (cleanUrls=false,trailingSlash=undefined) are still lenient and expose risks to relative references issues if accessed from the incorrect path.
    • Vercel (cleanUrls=false,trailingSlash=false), (cleanUrls=true,trailingSlash=undefined), (cleanUrls=true,trailingSlash=false), and (cleanUrls=true,trailingSlash=false) has many relative references issues.

To be fair to Vercel, their configuration works best if the respective framework used to deploy also supports a trailingSlash: boolean option. But if you're using a framework without the option, or if you're uploading mixed file.html and folder/index.html files manually, it's easy to get into the wrong configuration that messes with your site.

For server-rendered pages, as long as your server followed the guide above regarding relative references and redirects, you're good to go. However, keep in mind that some hosting providers, like Vercel, may rewrite the paths before reaching your server (in Vercel's case, the trailingSlash option affects it). And for providers that also support hosting your server, in most cases, its static file serving will take a higher priority before hitting your server, which can also sometimes be configurable.

Servers

Server/file/file//file.html/folder/folder//folder/index.html/both/both//both.html/both/index.html
express💢 404💢 404➡️ /folder/➡️ /both/
sirv
http-server💢 404➡️ /folder/
deno (file-server)💢 404💢 404➡️ /folder/➡️ /both/
python -m http.server💢 404💢 404➡️ /folder/➡️ /both/
httpd (apache)💢 404💢 404➡️ /folder/➡️ /both/
nginx💢 404💢 404➡️ /folder/➡️ /both/

Dimmed cells indicate that the path may lead to relative references issues or they return an unexpected content.

If the user chooses to deploy on their own server, they may be using one of these to serve the HTML files. Note that none of the cells have clickable links (as they're tedious to deploy), but you can run the tests yourself at https://github.com/bluwy/trailing-slash-servers.

  1. There's a consistent and equal behaviour among express, deno (file-server), python -m http.server, httpd (apache), and nginx.
    • They are able to serve all files correctly without causing relative references issues.
    • However, they don't serve the same file with a pretty URL, nor handle /file as an alias of /file.html.
    • Due to not supporting the /file alias, it'll not recognize /both as /both.html and performs a redirect to /both/ instead.
  2. sirv serves all files very leniently and exposes risks to relative references issues if accessed from the incorrect path.
  3. http-server works quite similar to express and others, with a plus that it supports /file. But for /both, it returns the /both/index.html file instead.
  4. If we ignore the both tests since they're rare, then:
    • All servers are fairly decent, except sirv which exposes risks to relative references issues if accessed from the incorrect path.

Frameworks

Conclusion

Trailing slashes can be tricky, but as long as you follow the guidelines above, you and your users should be able to get a consistent deployment experience on any hosting provider.

For a final recap:

  1. Only allow trailing slash or no trailing slash globally or per-route. Avoid supporting paths with both trailing slash and no trailing slash for the same content.
  2. Ensure relative references in a HTML file is correct depending on whether the file is served with or without a trailing slash (inferred from the rules of pretty URLs).
  3. If you decide to support both trailing slash and no trailing slash for a route, make sure that relative references still work, and inform users of the caveats.
  4. Where possible, return a 301 redirect to the correct path instead of a 404 not found for incorrect paths.
  5. Make sure to test against hosting providers if you're unsure how certain routes interact. Certain providers may have unique behaviours or configurations that allow you to achieve what you want.

And that's it! May you deal with trailing slash once and never again.

Additional resources