Using Next.js Without Vercel
1/2/2022
I previously wrote about the decision to deploy a React app using Next.js on CloudFront to run this blog post. Everything worked great until I refreshed a page and got a 403 error back from CloudFront.
Both in the development and production environments, I could click from one link to the next, and use the browser back and forward buttons. In dev, I could hit refresh with no problems. But in production, it was a different story. What was going on?
To answer that question, we first need to talk briefly about static generation, dynamic routing, and exporting HTML with Next.js.
Static Generation and Dynamic Routing
Static Generation is a form of pre-rendering in Next.js. HTML is generated at build time. There are two flavors of Static Generation: with and without data. I'm using the "with data" flavor, because at build time Next.js is reading a directory, parsing front matter out of files, rendering Markdown as JSX and building a dynamic list of pages.
The "dynamic routing" comes into play with my file at pages/blog/[post].js
. Any request made for /pages/blog/foo
is going to go to pages/blog/[post].js
, and foo
will be the value of a query parameter named post
. The script at [post].js
(that's literally its name) can then dynamically build content based on that query parameter's value.
Exporting HTML with Next.js
The next export
command, when run after a next build
, will create an HTML version of your app in a directory named out
. You can then upload the contents of the out
directory to your S3 origin, and your site is deployed.
Note the address of this page in your browser bar. It doesn't end in .html
. So what's going on? Next.js has embedded JavaScript in the page you're looking at. Using Next.js's routing system, content is rendered client-side by making requests to the CloudFront backend for data. So, as you click from page to page, your browser is updating the DOM using JavaScript.
What Happens on Page Reload
When you reload the page, you're telling your browser to make a new request back to the origin for the URL in your browser's location bar. And that's the problem. In the case of the page you're looking at right now, there's no file at /pages/blog/using-next-without-vercel
. So, CloudFront has no content to show you, and you get an error back.
There is, however, a file at /pages/blog/using-next-without-vercel.html
. Note the .html
ending. Next.js made that page when I ran net export
. What good are these .html
files if none of the links generated in the app actually use them?
The HTML content has to be there. The problem is, I'm not using Vercel. Vercel would make it all work just fine, because that's what it's for. I'm using S3 and CloudFront, which seems to be a bit of an edge case.
In a Stack Overflow post on CloudFront and Next.js, one person commented that he and his team "fought hard" to stay on CloudFront out of "ego," but eventually realized they were trying to build their own Vercel, and gave in. I'm not there yet.
CloudFront Functions to the Rescue
All we really need to do is tell CloudFront: "if someone requests a URL and it doesn't end in a slash or an extension, then tack on .html
." That sort of thing previously could only be done with Lambda@Edge, but AWS introduced CloudFront Functions in May 2021, which make this sort of thing easier and cheaper.
So, I created a CloudFront Function to do that very thing and attached it to the view request phase of my CloudFront distribution. Here is the code:
function handler(event) {
var request = event.request;
var regex = /\.\w{1,4}+$/
if (!request.uri.match(regex) && !request.uri.endsWith('/')) {
request.uri += '.html'
}
return request;
}
Conclusion
Next.js is made to work on Vercel, which stands to reason. However, it was possible for me to get things working well enough without it for my particular use case. Note that I did try a few other things, but could never arrive at a solution that would work both in the development environment and in production. Ultimately, I started to think about how Vercel might solve this problem, and hit upon a simple answer.