Skip to main content

NextJS static export routes don't work by default

· 4 min read

You may be wondering why your subroutes on your NextJS site only work in the development environment when built with output: 'export'.

This 5+ year old issue is curious... read on for the solution AND the logical explanation.

Static Export Behavior

Often times when someone wants to statically export a NextJS app they are looking to host on a CDN or a very simple zero-configuration web server.

They are not looking to write routing rules in Nginx or Apache for every subroute.

That is why it is curious that, by default, output: 'export' generates subroute pages by placing subroute.html in the root folder.

This is a problem because the links (created by NextJS's own Link React component) output an anchor with href /subroute, which will inherently be broken.

Official NextJS remedy

NextJS references this issue in one other part of their documentation: Static Export Deployment

It offers a bit of simple Nginx code to rewrite URLS:

  location /blog/ {
rewrite ^/blog/(.*)$ /blog/$1.html break;
}

However, on a CDN that is only designed to serve assets... we do not have the ability to modify such rules unless we create our own HTTP Proxy.

Unofficial Remedy

A 5 year old reddit post pointed the way to an obscure config setting that remedies the issue, but at the cost of creating another one.

A seemingly irrelevant setting called trailingSlash is the culprit. Initially, the documentation appears to only talk about setting a literal trailing slash on the end of URLs. But a keen eye will notice something seemingly out-of-place at the very end of this documentation:

When used with output: "export" configuration, the /about page will output /about/index.html (instead of the default /about.html).

This setting is referenced nowhere on the Static Generation documentation, so why is a seemingly obviously needed feature obscured?

Implications and the Bigger Picture

The reason is because of another little wrench thrown in by using trailingSlash: true -

While trailingSlash: true remedies the issue by appending a / to anchor hrefs AND building subroutes as subroute/index.html - links like /subroute are still broken.

So now you have a situation where /subroute/ works but /subroute does not. While this might make sense in a file system paradigm, it goes against modern HTTP routing conventions.

By understanding this, we can now see a correct way to solve this problem is to have an HTTP Proxy configured with rewrites in front of a CDN.

Now that we have the bigger picture, our journey continues... this paradigm has already been thought of by many CDN services - which often act as object stores and not traditional file systems.

The next section describes AWS's approach.

Notes on AWS S3 Static Web Hosting

S3 routing acts as of a object file store rather than a traditional file system. This means that its logic when it comes to trailing slashes is handled in modern way.

When you enable S3 Static Web Hosting, AWS produces a simple HTTP server to serve your content. Its routing logic respects the modern paradigm.

/subroute and /subroute/ both point to the same object. If that object is a folder, then its index.html is served.

Cloudfront with S3

If you link S3 as an origin point for Cloudfront, by default Cloudfront picks its bucket endpoint - which is a REST service. This means that trailing slashes get removed as they don't make sense on REST endpoints.

To make CloudFront work a NextJS project configured with trailingSlash: true and exported to S3, the CloudFront origin needs to be linked to S3s static website endpoint, not its bucket endpoint. i.e.: bucket-name.s3-**website**-region.amazonaws.com