Next.JS is a fairly nice way of building a multi-page, mostly statically rendered website with React and making it make sense. It actually solves the problem of “what if a React app was not a Single Page Application” pretty well, but it’s somewhat particular about how it wants to be deployed.

To be precise, it wants to run as a Node.js process on your server. Unfortunately, I have a bit of an aversion to running node processes on my server1 so I wanted to avoid that. Luckily, this is actually a somewhat supported process. Today I’d like to elaborate on the “somewhat” bit, and hopefully help someone else looking to solve the same problem.

Creating a static export

Let’s start with the good part: Next.JS actually has a pretty nice static export feature that allows you to generate a static version of a website, and ship it as just a bunch of files. This is great because it avoids running an additional web server process just to serve an ostensibly static website.

In previous versions of Next.JS, the export was made through the next export command. You’d still need to next build first, making it somewhat inconvenient. In version 13.3 however, the static export became a configuration setting. To be clear, this article explains how to set this up for Next.JS 13.3 and up.

To start, we need to update our next.config.js file to create a static export as its build result. This s simple enough:

1
2
3
4
5
6
7
8
/**
 * @type {import('next').NextConfig}
 */
const nextConfig = {
  output: 'export',
};
 
module.exports = nextConfig;

That is actually all. An out folder should not contain the static export of your website.

Image optimization

Except it might not be. If you use any images on your website at all. You will be greeted with the following message instead:

Image Optimization using the default loader is not compatible with export.

By default, Next.JS tries to serve images that are optimized for the specific resolution and quality preferences of the user. This, in theory, saves data, but it also has the potential to slow things down due to reducing cache hit rates and generally introduces complexity.

Next.JS provides support for external optimization services which you can use, but in most cases, it offers a very minor performance improvement. My recommendation is therefore to make an appropriately sized WebP] export of your images, either lossy or lossless, and forego optimization altogether.

The error message already explains how to disable image optimization, but for completeness, the final configuration file looks like this:

1
2
3
4
5
6
7
8
9
10
11
/**
 * @type {import('next').NextConfig}
 */
const nextConfig = {
  output: 'export',
  images: {
    unoptimized: true,
  },
};
 
module.exports = nextConfig;

Other unsupported features

Even with the issues regarding images fixed, there are still some things that don’t work. In general, features that require configuration of the response beyond its contents won’t work, be it headers or status code. You also cannot run server side code, because there is no server.

Configuring Nginx

With our archive of files in hand, we can now try to configure Nginx to host it. This is almost straightforward, except that, while Next.JS likes to have URLs without file extensions, but the export does generate files with file extensions. The file for a URL like /page will be /page.html, and occasionally for reasons I don’t quite understand /page/index.html.

It’s possible to fix this with Nginx configuration, by using the try_files feature. The only other thing that needs configuring is the 404 page, which is conveniently generated as 404.html. With that in mind, our (simplified) configuration looks as follows:2

1
2
3
4
5
6
7
8
9
10
11
12
server {
    listen 80;

    root /your/web/root/;
    index index.html;

    error_page 404 /404.html;

    location / {
        try_files $uri $uri/ $uri.html =404;
    }
}

Now we can upload the files generated before to our web root, and the website should be served the way we expect it to.

In conclusion

While it’s a shame that the default way to host Next.JS is a dedicated Node process, it’s pleasantly simple to statically host it statically on a shared web server. Not just that, but ever since I started using the feature in January of this year, the list of unsupported features has steadily shrunk.

As of now, it seems that most features that could reasonably be supported by some static files is actually supported. Things that require setting custom headers, status codes, or even doing server side work obviously isn’t there, but if your website is actually static, most things will actually work.

So with that in mind, I encourage anyone to try to ditch their Node.js servers and run everything through a nice and boring Nginx server.


  1. You might even say I have an aversion to running javascript anywhere, but this very website and the article you’re reading would prove otherwise. We could still use less of it. ↩︎

  2. The export documentation for Next.JS currently has a sample Nginx configuration very much like the one listed. I am still adding mine as the original documentation I worked from back in January didn’t have one and I think my simplified version is better. ↩︎