4

I have an application that uses NextJS as a wrapper, and I make use of NextJS's dynamic routing feature. I had a problem when deploying it to CloudFront due to dns.com/path/page not being rendered, instead CloudFront expected it to be dns.com/path/page.html. I worked it around by applying this lambda-edge-nice-url solution. It works properly now. However, there's still one issue left: NextJS's dynamic routes. dsn.com/path/subpath/123 should work, since 123 is a dynamic parameter. However, that does no work. In only returns the page when I access dns.com/path/subpath/[id], which of course is not correct, since [id] is not a parameter I want to load.

The strangest thing is: if I try to access the URL as I stated above directly, it fails. However, inside the application I have buttons and links that redirect the user, and that works properly.

Navigating from inside the application (button with router.push inside its callback): enter image description here

Trying to access the url directly: enter image description here

Can anyone help me to properly route the requests?

4 Answers 4

3

After trying a lot of different code, I finally came up with a Lambda edge expression that fixed two issues in one:

  • The need to insert .html at the end of the URL
  • NextJS dynamic routes that were't working on refresh or when accessed directly to URL.

The code below basically takes care of dynamic routes first. It uses a regex expression to understand the current URL and redirect the request to the proper [id].html file. After that, if the none of the regex are matched, and if the URL does not contain .html extension, it adds the extension and retrieves the correct file.

const config = {
    suffix: '.html',
    appendToDirs: 'index.html',
    removeTrailingSlash: false,
};

const regexSuffixless = /\/[^/.]+$/; // e.g. "/some/page" but not "/", "/some/" or "/some.jpg"
const regexTrailingSlash = /.+\/$/; // e.g. "/some/" or "/some/page/" but not root "/"
const dynamicRouteRegex = /\/subpath\/\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b/; // e.g /urs/some-uuid; // e.g. '/subpath/uuid'

exports.handler = function handler(event, context, callback) {
    const { request } = event.Records[0].cf;
    const { uri } = request;
    const { suffix, appendToDirs, removeTrailingSlash } = config;
    
    //Checks for dynamic route and retrieves the proper [id].html file
    if (uri.match(dynamicRouteRegex)) {
        request.uri = "/subpath/[id].html";
        callback(null, request);
        return;
    }
    
    
    // Append ".html" to origin request
    if (suffix && uri.match(regexSuffixless)) {
        request.uri = uri + suffix;
        callback(null, request);
        return;
    }
    
    // Append "index.html" to origin request
    if (appendToDirs && uri.match(regexTrailingSlash)) {
        request.uri = uri + appendToDirs;
        callback(null, request);
        return;
    }

    // Redirect (301) non-root requests ending in "/" to URI without trailing slash
    if (removeTrailingSlash && uri.match(/.+\/$/)) {
        const response = {
            // body: '',
            // bodyEncoding: 'text',
            headers: {
                'location': [{
                    key: 'Location',
                    value: uri.slice(0, -1)
                 }]
            },
            status: '301',
            statusDescription: 'Moved Permanently'
        };
        callback(null, response);
        return;
    }

    // If nothing matches, return request unchanged
    callback(null, request);
};

Many thanks to @LongZheng for his answer. For some reason his code did not work for me, but it might for some, so check his answer out. Also, big shoutout to Manc, the creator of this lambda-edge-nice-urls repo. My code is basically a mix of both.

Sign up to request clarification or add additional context in comments.

4 Comments

Hey @Pelicer, so the pages using dynamic routes in your application on S3 bucket or are they server-side-rendered? I want to cache the responses of server side rendered pages using dynamic routes on AWS. Is that possible?
They are hosted on a S3 bucket
This is really helpful - thank you for the detailed post. One thing that I'm confused on is the dynamic route part. In the code above you rewrite the route to /subpath/[id].html and I'm confused about that. I'm thinking that would literally send a request to /subpath/[id].html to Cloudfront which would then give an error. I checked my out directory and I have the following path /subpath/[id]/index.html. any help or expansion you could offer would be very appreciated.
@ngriffin check your trailingSlash value in next.config.js. This setting controls that export option. Docs: <nextjs.org/docs/api-reference/next.config.js/trailing-slash>. You are correct that you generally have be mindful of which is in play when writing an edge lambda for NextJS :)
2

I use a CloudFront Lambda@Edge origin request function to handle re-writing both my dynamic routes as well as static routes to the appropriate HTML file so that CloudFront can serve the intended file for any paths.

My lambda function looks like

export const handler: CloudFrontRequestHandler = async (event) => {
    const eventRecord = event.Records[0];
    const request = eventRecord.cf.request;
    const uri = request.uri;

    // handle /posts/[id] dynamic route
    if (uri === '/posts' || uri.startsWith('/posts/')) {
        request.uri = "/posts/[id].html";
        return request;
    }
    
    // if URI includes ".", indicates file extension, return early and don't modify URI
    if (uri.includes('.')) {
        return request;
    }

    // if URI ends with "/" slash, then we need to remove the slash first before appending .html
    if (uri.endsWith('/')) {
        request.uri = request.uri.substring(0, request.uri.length - 1);
    }

    request.uri += '.html';
    return request;
};

1 Comment

Hi, @LongZheng. Your code did not work for my scenario. However, it did put me in the right path. +1. I'll post the code that actually did the trick for me in another answer. Thanks!!!
1

The solution mentioned by @Pelicer is not really scalable beyond their solution and restricts how you name your path params. Instead a similar approach would be to use a dynamically generated file of routes. With NextJS if you run the build command it will output a routes manifest at out/.next/routes-manifest.json. This file will look something like

{
"version": 3,
"pages404": true,
"basePath": "",
"redirects": [
    {
        "source": "/:file((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/]+\\.\\w+)/",
        "destination": "/:file",
        "internal": true,
        "statusCode": 308,
        "regex": "^(?:/((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/]+\\.\\w+))/$"
    },
    {
        "source": "/:notfile((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/\\.]+)",
        "destination": "/:notfile/",
        "internal": true,
        "statusCode": 308,
        "regex": "^(?:/((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/\\.]+))$"
    }
],
"headers": [],
"dynamicRoutes": [
    {
        "page": "/test-path/[testPathId]",
        "regex": "^/test\\-path/([^/]+?)(?:/)?$",
        "routeKeys": {
            "testPathId": "testPathId"
        },
        "namedRegex": "^/test\\-path/(?<testPathId>[^/]+?)(?:/)?$"
    }
],
"staticRoutes": [
    {
        "page": "/",
        "regex": "^/(?:/)?$",
        "routeKeys": {},
        "namedRegex": "^/(?:/)?$"
    },
    {
        "page": "/home",
        "regex": "^/home(?:/)?$",
        "routeKeys": {},
        "namedRegex": "^/home(?:/)?$"
    }
],
"dataRoutes": [],
"rsc": {
    "header": "RSC",
    "varyHeader": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
},
"rewrites": []} 

This gives us the dynamic routes that nextjs generates for use in the static generated app. We can then write a simple CloudFront Lambda@Edge function to quickly map the request when it comes in. The following code would read the above json manifest and reroute the request to the correct S3 path.
Note: There is some additional reuse that could be added here between the static and dynamic routes.

exports.handler = function (event, context, callback) {
let routes = require('./routes-manifest.json');
const { request } = event.Records[0].cf;
const { uri } = request;
const {dynamicRoutes, staticRoutes} = routes;
const appendToDirs = 'index.html';

if(!uri || uri === '/' || uri === ''){
  callback(null, request);
  return;
}
dynamicRoutes.forEach(route => {
  if(uri.match(route.regex)){
    if(uri.charAt(-1) === "/"){
      request.uri = route.page + appendToDirs;
    } else {
      request.uri = route.page + "/" + appendToDirs;
    }
    callback(null, request);
    return;
  }
});
staticRoutes.forEach(route => {
  if(uri.match(route.regex)){
    if(uri.charAt(-1) === "/"){
      request.uri = route.page + appendToDirs;
    } else {
      request.uri = route.page + "/" + appendToDirs;
    }
    callback(null, request);
    return;
  }
});
// If nothing matches, return request unchanged
callback(null, request);};

Comments

1

In 2021 AWS introduced CloudFront Functions (https://aws.amazon.com/blogs/aws/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/ ) This is much cheaper than Lambda@Edge and there's a generous free quota.

With terraform you can do something like:

resource "aws_cloudfront_function" "rewrite_uri" {
  name    = "rewrite_uri"
  runtime = "cloudfront-js-1.0"
  comment = "Implement dynamic routes for Next.js"
  publish = true
  code    = <<EOF
function handler(event) {
    var request = event.request;
    request.uri = request.uri.replace(/^\/something\/[^/]*\/edit$/,              "/something/[something_id]/edit");
    return request;
}
EOF
}

resource "aws_cloudfront_distribution" "page" {
...
 default_cache_behavior {
    ....
    function_association {
      event_type   = "viewer-request"
      function_arn = aws_cloudfront_function.rewrite_uri.arn
    }
 }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.