Nacho's Blog

How To Fix Next.js Exporting Pages As Files

One of the not so well known features of Next.JS (only with the Pages router...) is the ability to export the page as static HTML. This is very useful if you want all the benefits of UI development with React and SSR. But don't want the complexity and cost of SSR deployments. Thant is the case for this blog for example. Prerendering posts is easy and allows me to easily create new post pages, but having function and database calls of every user might get expensive for just a little HTML, so it's all just exported as static HTML and served.

The Problem

When exporting a dynamic page, Next.JS creates a folder with multiple HTML files within it for every page. This works fine so long as you are navigating within the site and having the hydrated router loaded. But as soon as you directly type the URL into the browser it will return 404 because while the folder exists, there is no index file for it and the url doesn't contain the .html extension on it (and no one has typed .html into a url since 2006)

This problem can show as next not exporting pages as index, not exporting pages into their own folders, not creating index files. (yes, this paragraph is for people to find this in google)

The Solution

Some places recommend using the backSlash option then exporting, and that works, but only if you have the hydrated router running. It doesn't work for first loads. It seems that vercel doesn't have a solution yet, maybe once they enable exporting for the app router 🙏.
The Real solution to me was just writing a quick script that finds all html files not called index, places them in a folder with their name and renames them to index.

The Script

const fs = require("fs");
const { globSync } = require("glob");

try {
  const pages = globSync("./out/**/*.html", {
    ignore: ["./out/**/index.html", "./out/index.html"],
  for (const p of pages) {
    const [filename, ...path] = p.split("/").reverse();
    const [name, ext] = filename.split(".");
    if (name === "index") {
    fs.renameSync(p, `${path.join("/")}/${name}/index.${ext}`);
} catch (err) {
  throw new Error(err);

The script could probably not have the for loop if you integrate it into a pipeline and have async support for it. I'm just running it as is chained to the export command, so I needed it to be sync.

You can also just import it from NPM from here

Published: Tuesday, Sep 5, 2023
Privacy Policy© 2023 Ignacio Degregori. All rights Reserved.