Next.js Rewrites, Amazon S3, and Trailing Slashes
Published on 21 December 2020This post talks about how to set up Next.js rewrites to point to a static site hosted on Amazon S3, in particular how to deal with trailing slashes.
Goal
The problem that I’m describing occurred in the process of creating Memm, a tool for studying for the MCAT.
The goal: move blog.memm.io => memm.io/blog (apparently better for SEO purposes).
I want memm.io
to continue hosting the main site, while only paths under
memm.io/blog
will actually point to a different underlying endpoint, e.g.,
http://MY-S3-BUCKET.s3-website-us-west-2.amazonaws.com/
.
Problem
When navigating to memm.io/blog
and clicking on a link, everything worked
fine. However, if the page was refreshed at memm.io/blog/my-slug
, it would
redirect to memm.io/my-slug
. This also meant that linking directly to a blog
post would not work, changing the path strangely.
Additionally, my Gatsby had its pathPrefix
config
set to /blog
, which meant there were a variety of static assets that were
supposed to be loaded (e.g., http://MY-S3-BUCKET.s3-website-us-west-2.amazonaws.com/foobar.css
) that were
similarly 404ing due to Next.js’s strange rewriting behavior.
This is a problematic next.config.js (see Next.js documentation for where this goes).
async rewrites() {
return [
{
source: '/blog/:slug*',
destination: `${BLOG_URL}/:slug*`,
},
];
},
Solution
This leverages a new feature supported in Next 9.5 and above called rewrites.
We’ll explore more in depth why this is happening, but since most people reading
articles just want the solution, I fixed it with something like this in my
next.config.js
{
async rewrites() {
return [
{
source: '/blog/:slug*/',
destination: `${BLOG_URL}/:slug*/`,
},
{
source: '/blog/:slug*',
destination: `${BLOG_URL}/:slug*`,
},
];
},
// Causes next.js to add trailing slashes to end of URLs.
trailingSlash: true,
}
Using withCss, withSass, withLess
It should also be noted that my next.config.js
uses Ant Design, and therefore
it has a bit of fiddling with the CSS/Sass/Less loading. In another possible bug with the
@zeit/next-css
package, trailingSlash
does NOT work within the nested withCss
call, although
strangely rewrites works. This is probably also a bug given that rewrites
works but trailingSlash
doesn’t, although I didn’t file it –
withCss
is technically a deprecated package; they shouldn’t be
expected to support newer features of Next.js to be compatible with it.
Note how in the code below that rewrites is within the withSass
but the
trailingSlash
must be outside of the withCss
call in order to work.
const withCss = require('@zeit/next-css');
const withLess = require('@zeit/next-less');
const withSass = require('@zeit/next-sass');
const BLOG_URL = 'http://MY-S3-BUCKET.s3-website-us-west-2.amazonaws.com';
module.exports = {
...withCss(
withSass({
...withLess({
// Other stuff...
}),
// https://nextjs.org/docs/api-reference/next.config.js/rewrites#rewriting-to-an-external-url
// https://github.com/vercel/next.js/issues/14930
// In the end, we need this with trailing-slash to work properly. Otherwise
// it does a 302 and when you refresh blog pages it messes up.
async rewrites() {
return [
{
source: '/blog/:slug*/',
destination: `${BLOG_URL}/:slug*/`,
},
{
source: '/blog/:slug*',
destination: `${BLOG_URL}/:slug*`,
},
];
},
})
),
trailingSlash: true,
};
Cause
I’m still not 100% confident in my explanation of the issue, but I think it goes something like this:
- Next.js by default removes a trailing slash when it doesn’t
exist, according to the trailing slash
documentation. That means
memm.io/blog/
becomesmemm.io/blog
under the default settings. - Amazon S3 in contrast, does a 302 redirect when there is no trailing slash. I think that Next.js rewrites doesn’t play nicely with 302. This is also mostly unconfigurable: see this StackOverflow question
Somehow, with the problematic config, this causes two redirects that causes
Next.js to remove the /blog/
from /blog/my-slug
and simply go to /my-slug
. It’s
possible this is a bug with Next.js rewrites, but it could also be an intended
interaction given the quirks of the two opposing redirect rules.