Trailing Slash and basePath in Next.js Static Export
A Next.js static export is a folder of files, and how those files are named decides whether URLs work on a given host. Two settings in next.config.js control it. trailingSlash decides whether /about is written as about.html or about/index.html. basePath decides whether the whole site lives at the domain root or under a subpath such as /docs. Get either one inconsistent with the host, the canonical URLs or the links in content, and the result is a site that works in next dev, works on one host, and produces 404s, redirect chains or duplicate URLs on another.
This guide explains what each setting does to the exported files and links, which combination works on which host, and how to verify that links, assets, canonicals and host behaviour agree. It is part of Next.js Static Export for Content Sites. The examples come from a 420-page marketing site that moved from Vercel to S3 and CloudFront, and a documentation site served under /docs on a company domain.
Prerequisites
- Next.js 14 or 15 with
output: 'export'. - Knowledge of the target host's rules for directory indexes and extension-less URLs.
- A crawler or link checker for the exported site, run against a local static server that mimics the host.
What trailingSlash Does to the Output
With the default trailingSlash: false:
out/index.html
out/about.html
out/blog/hello-world.html
Links rendered by next/link point to /about and /blog/hello-world. The site only works if the host serves about.html for a request to /about. Vercel and Netlify do; Cloudflare Pages and Workers static assets do by default; plain S3 website hosting, GitHub Pages in some configurations, and nginx without a try_files rule do not.
With trailingSlash: true:
out/index.html
out/about/index.html
out/blog/hello-world/index.html
Links point to /about/. Every static host serves index.html for a directory request, so this layout works everywhere. On the S3 migration, switching from false to true was the difference between 419 of 420 pages returning 403 errors and all of them working without any CloudFront function.
Making Every Layer Agree
Choosing a layout is half the job. The other half is making sure every place that writes a URL uses the same form:
| Layer | What to check |
|---|---|
next/link hrefs | Next.js adds the slash automatically when trailingSlash: true |
| Links in MDX or CMS content | Plain Markdown links are not rewritten; /pricing stays /pricing |
| Canonical URLs (Metadata API) | build from the same path helper, with slash |
| Sitemap URLs | same helper, as in Generating Sitemaps and RSS in Next.js Static Export |
| Host behaviour for the other form | redirect /about to /about/ with a 301 |
Content links are the usual leak. On the marketing site, 780 links in MDX and CMS rich text pointed to slash-less URLs. With trailingSlash: true and a host that redirected, each was a redirect hop on every click. A small remark plugin normalized internal links in MDX at build time, and a CMS-side rich-text serializer did the same for landing pages. The link checker in CI now treats internal redirects as errors, as described in Checking Links in Pull Requests.
What basePath Does
basePath: '/docs' prefixes every route. app/page.tsx becomes /docs/, next/link href="/install" renders as /docs/install/, and _next assets load from /docs/_next/…. The exported files do not move into a docs/ directory, though: out/index.html is still at the root of out/, and it is the deployment's job to serve out/ at /docs/.
// next.config.js
module.exports = {
output: 'export',
trailingSlash: true,
basePath: '/docs',
images: { unoptimized: true }
};
Three things do not get the prefix automatically: plain <a href="/…"> elements, links in Markdown or CMS content, and asset paths written as strings (<img src="/logo.svg">). All of them need the prefix added, either with a helper that prepends process.env.NEXT_PUBLIC_BASE_PATH or by resolving them in the content pipeline. The documentation site had 112 such references; a grep for href="/ and src="/ in the exported HTML that do not start with /docs/ found them all.
GitHub Pages Project Sites
A GitHub Pages project site lives at https://<user>.github.io/<repo>/, which is exactly the basePath case. Set basePath to /<repo> and trailingSlash: true, and add an empty .nojekyll file to out/ so Pages does not ignore the _next directory (directories starting with an underscore are skipped by the classic Jekyll processing). With the Actions-based Pages deployment, actions/configure-pages can provide the base path as an output, which keeps the repository name out of the config file. See Deploying to GitHub Pages with Actions.
assetPrefix Is Something Else
assetPrefix changes only where /_next/static/… files are loaded from — for example https://cdn.example.com — while page URLs stay on the main domain. It is useful when assets are uploaded to a separate bucket or CDN hostname. It does not affect routing, links, or images referenced by path, and combining it with basePath is rarely needed. When both are set, the asset URL is assetPrefix plus basePath plus /_next/…, which surprised the documentation team when their CDN rules expected no /docs in asset paths.
Verifying Before Deploy
Serve out/ locally the way the host will serve it and crawl it:
# emulate a host that serves out/ under /docs/ with directory indexes
mkdir -p /tmp/site/docs && cp -r out/. /tmp/site/docs/
npx serve /tmp/site --no-clipboard --listen 4000 &
npx linkinator http://localhost:4000/docs/ --recurse --silent
Then check the invariants with a script over the HTML: every internal href starts with the base path and ends with a slash (or is a file with an extension); every canonical equals the page's own URL; no asset URL is missing the prefix. On the documentation site, this caught the 112 unprefixed references before the first deploy.
Changing the Setting on a Live Site
The marketing site had been live for three years with trailingSlash: false on Vercel, where both forms worked, so external links used both. Moving to trailingSlash: true for S3 meant every slash-less URL needed a 301. CloudFront served the directory layout natively, and a single CloudFront Function redirected any path without a slash or file extension to the slashed form, as in CloudFront Functions for Redirects. Canonicals, sitemap and internal links switched in the same deploy. Search Console showed the slash-less URLs replaced by the new canonicals within about three weeks, with no measurable drop in clicks.
Measured Impact
| Measure | Before | After |
|---|---|---|
Pages returning 403 on plain S3 (trailingSlash: false) | 419 of 420 | 0 |
| Internal redirect hops per session (RUM, marketing site) | 2.3 | 0.1 |
Unprefixed links on the /docs site | 112 | 0 |
| Search Console "Page with redirect" for internal links | 316 | 4 |
| CloudFront functions needed for clean URLs | 1 | 0 |
Pitfalls & Rollback
- Switching
trailingSlashon a live site. Every URL changes; add 301 redirects from the old form and update canonicals together. - Assuming
next/linkcovers content links. Markdown and CMS links need their own normalization. - Missing
.nojekyllon GitHub Pages._nextis skipped and the site renders without JavaScript or CSS. - Mixing
assetPrefixandbasePathwithout checking CDN paths. Asset URLs include both. - Rollback: both settings are one line each; reverting them restores the previous layout on the next build, along with the redirects that point back.
Conclusion
In a Next.js static export, trailingSlash: true is the portable choice because directory indexes work on every host, and basePath is required whenever the site does not live at the domain root. Neither setting reaches links written in content or raw markup, so normalize those in the content pipeline, check the exported HTML for the invariants, and crawl a local server that mimics the host before deploying.
FAQ
What does trailingSlash do in a Next.js static export?
With trailingSlash true, each route is written as a directory containing index.html, for example out/about/index.html, and links point to /about/. With the default false, routes are written as about.html and links point to /about, which relies on the host to serve about.html for that path.
Which setting works on the most hosts?
trailingSlash true. Every static host serves directory index files, while serving about.html for a request to /about is a host-specific feature that GitHub Pages, S3 website hosting and some CDNs handle differently or not at all.
When do I need basePath?
When the site is served under a subpath rather than at the domain root, such as a GitHub Pages project site at /repo-name/ or documentation mounted at /docs/ behind a reverse proxy. basePath prefixes every route and link generated by Next.js.
What is the difference between basePath and assetPrefix?
basePath changes the URL of pages and everything under them. assetPrefix only changes where the _next static assets are loaded from, typically to serve them from a separate CDN domain, while page URLs stay the same.
Related
- Parent: Next.js Static Export for Content Sites — the static export model.
- Clean URLs and Trailing Slashes on S3 — the host side of the same problem.
- Canonical URLs and Duplicate Content on Static Sites — why the forms must agree.
- Handling Dynamic Routes in Next.js Static Export — generating every route.
- Using MDX in Next.js Static Export — where content links are normalized.
Filed underHow-tosNext.jsAWS (S3 and CloudFront)