NextJS static export routes don't work by default
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
