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, and this page is available as Markdown at https://rspress.rs/blog/trailing-slash.md.
close
  • English
  • 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:

    <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/:

    /next/guide/start/quick-start

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

    /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 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:

    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 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:

    /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 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/
    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.

    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 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