> For AI agents: the complete documentation index is available at https://rspress.rs/llms.txt, the full documentation bundle is available at https://rspress.rs/llms-full.txt.

_August 3, 2026_

# The trailing slash problem in static sites

## Discovering the problem

The story begins with a real issue encountered by an Rspress user.

The user deployed a site under `/next/` and used a relative link on the homepage:

```html
<a href="./guide/start/quick-start">Quick start</a>
```

At the domain root, the link worked whether or not the URL ended with `/`. It also resolved correctly when the user visited `/next/`:

```text
/next/guide/start/quick-start
```

However, when the user visited `/next`, the link resolved to:

```text
/guide/start/quick-start
```

The page itself loaded successfully, so why did the relative link lose the `/next` segment?

This problem led me to ask: what difference does a trailing slash actually make?

### The root URL is a special case

[RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.3) defines an empty path as equivalent to `/` for HTTP(S) URLs. Therefore, even if you enter `https://example.com`, the browser still sends `GET /`.

### A trailing slash changes relative URL resolution

The browser resolves relative links against the current page URL. It treats the final segment of `/next` as the current resource name, while `/next/` means that the current resource is inside a directory:

```js
new URL('./guide/start', 'https://example.com/next').pathname;
// => /guide/start

new URL('./guide/start', 'https://example.com/next/').pathname;
// => /next/guide/start
```

At the client-router layer, URL matching is usually more permissive than file lookup on a Web server. For example, [React Router](https://github.com/remix-run/react-router/blob/react-router%407.18.2/packages/react-router/lib/router/utils.ts#L1576-L1578) does not distinguish between paths with and without a trailing `/` by default. Both URLs below therefore match the same route whose path is `/guide`:

- `/guide`
- `/guide/`

This permissive behavior applies only to route matching; it does not mean that the client automatically normalizes the URL. React Router does not remove `.html` or treat `/index.html` as a directory index. Whether `/guide.html` and `/guide/index.html` are accessible still depends on additional handling by the framework or Web server.

This explains why “the page loads” and “relative links resolve correctly” are separate questions. Permissive matching only helps the client find a route; it does not update the URL in the address bar. As long as the address bar contains `/next`, the browser resolves relative paths using the first rule above.

The distinction originates from the early Web, where URL paths were commonly mapped to file systems. The following URLs could refer to two different static outputs:

```text
/guide.html       -> dist/guide.html
/guide/           -> dist/guide/index.html
```

When a URL points to a directory, a Web server usually appends a trailing slash before looking for `index.html`. Modern hosting platforms can use rewrites to serve the same page for multiple URLs, but this does not change how browsers resolve relative links.

## Solving the problem

The key is to keep the current URL in the address bar consistent with the preferred URL generated by the site.

Rspress [`route.cleanUrls`](https://rspress.rs/api/config/config-basic.md#routecleanurls) controls whether generated URLs include the `.html` extension. However, it only affects links generated by Rspress. It cannot prevent users from arriving at `/guide.html`, `/guide/`, or another variant through bookmarks, search results, or external websites.

Rspress therefore introduces `route.cleanUrlsRedirect` in version 2.1.0. During client startup, it first matches the actual route, then uses `history.replaceState` to update the address bar to the format selected by `cleanUrls`. For example, when `cleanUrls: true`:

- `/guide.html`, `/guide/`, and `/guide/index.html` → `/guide`
- `/reference/index.html` → `/reference/`

```ts title="rspress.config.ts"
import { defineConfig } from '@rspress/core';

export default defineConfig({
  route: {
    // Both options will be enabled by default starting in Rspress 2.1.0.
    cleanUrls: true,
    cleanUrlsRedirect: true,
  },
});
```

Normalization happens before the page renders, does not reload the page, and preserves the site `base`, query, hash, and existing history state. In the opening example, Rspress changes `/next` to `/next/` before rendering, so relative links no longer escape from `/next/`.

Starting with Rspress 2.1.0, both `cleanUrls` and `cleanUrlsRedirect` will be enabled by default. Client-side normalization cannot fully replace a server-side 301/308 redirect or solve duplicate-URL SEO problems on its own, but it can prevent application errors caused by inconsistent URL formats.

For the complete URL conversion rules and configuration details, see the [`route.cleanUrlsRedirect` API documentation](https://rspress.rs/api/config/config-basic.md#routecleanurlsredirect).

### Best practices

URLs no longer need to map directly to files on disk. Whether a site uses trailing slashes is a policy choice, not a question of correctness. Browsers and search engines still treat `/about` and `/about/` as two distinct URLs.

To prevent application errors and avoid indexing the same page under multiple URLs:

1. **Server:** Use 301/308 redirects to consolidate `/about` and `/about/` into a single preferred URL. Variants such as `/about.html` and `/about/index.html` can be handled at the same time. For example, Cloudflare Workers Static Assets supports [`auto-trailing-slash`](https://developers.cloudflare.com/workers/static-assets/routing/advanced/html-handling/) by default: regular HTML files use `/file`, directory indexes use `/folder/`, and other forms receive a 307 redirect to the corresponding canonical URL.
2. **Client:** Enable `cleanUrls` and keep a client-side redirect as a fallback. If the hosting platform cannot be configured with redirects, the client can write the preferred URL at the earliest page entry point. However, this runs only after the HTML and JavaScript load, so it cannot fully replace a server-side redirect.

**Keep every URL source consistent:** internal links, canonical URLs, sitemaps, and shared URLs should all use the preferred form. Other variants should redirect to it with 301/308 responses.

**Final checks:**

- `/file` and `/file/` should not both serve the same content as two independent URLs.
- Public URLs, static output, Rspress configuration, and CDN rules should follow the same trailing-slash policy.

## References

- [RFC 3986: URI Generic Syntax](https://www.rfc-editor.org/rfc/rfc3986)
- [URL Standard](https://url.spec.whatwg.org/)
- [Apache HTTP Server: DirectorySlash](https://httpd.apache.org/docs/current/mod/mod_dir.html#directoryslash)
- [Cloudflare Workers: HTML handling](https://developers.cloudflare.com/workers/static-assets/routing/advanced/html-handling/)
- [React Router: Path matching](https://github.com/remix-run/react-router/blob/react-router%407.18.2/packages/react-router/lib/router/utils.ts#L1576-L1578)
- [Google Search Central: To slash or not to slash](https://developers.google.com/search/blog/2010/04/to-slash-or-not-to-slash)
- [Why does the trailing-slash matter? A (very) short history](https://kentgigger.com/posts/why-does-the-trailing-slash-matter-a-short-history)
