Deploy Nextjs Static Website to AWS S3 and Cloudfront

aws s3

Learn how to deploy your Next.js static website to AWS S3 and Cloudfront in just a few simple steps. With this guide, you'll be able to optimize your website's speed and reliability, while reducing your hosting costs.


By deploying a Next.js static website to AWS S3 and Cloudfront, you can significantly enhance the performance and scalability of your website, while also decreasing your hosting expenses. In this tutorial, we will take you through the necessary steps to deploy your Next.js website to AWS S3 and Cloudfront. This blog is written with the following versions:

  • Next 13.3.0
  • Node 18
  • AWS SAM CLI 1.76

Next.js Static Hosting in AWS S3 and Cloudfront

Lets review the architecture for nextjs static website hosted on the AWS S3 and Cloudfront,

Nextjs Static hosted in S3 Deployment Architecture

For this particular architecture, we'll utilize a solitary AWS S3 bucket to store all the necessary files, including JavaScript, CSS, images, and the index.html files generated during the Next.js build process. To allow CloudFront to access the bucket content using origin policy and activate the static website hosting feature on the bucket, we'll need to modify the bucket permissions. Furthermore, AWS CloudFront will be employed to sit in front of the S3 bucket and help with edge caching of the files. By caching the files on CloudFront, we can limit read calls to S3 and boost our website's performance and security.

Next.js static export for Static Website Hosting

For next.js version 13.2 and below, the static export can be done in Next using the commands next build && next export. Running this command generate an out directory and the process is follows,

next export builds an HTML version of your app. During next build, getStaticProps and getStaticPaths will generate an HTML file for each page in your pages directory (or more for dynamic routes). Then, next export will copy the already exported files into the correct directory. getInitialProps will generate the HTML files during next export instead of next build.

Starting with Next 13.3, the next export will be deprecated and static export feature is enabled on the next.config.js by including output attribute as export output: 'export' During the next build process,it should generate the out directory containing the HTML/CSS/JS static assets. We can customize the output directory by mentioning the output directory if required in the next.config.js file using attribute distDir: 'dist'

  • nextjs13 output

AWS S3 and Cloudfront Creation using AWS SAM

Now lets talk about the AWS SAM template and see how we can deploy the static contents. In this tutorial, we are creating AWS S3, Cloudfront, Cloudfront function and then create a A record in route 53 for the cloudfront entry.

#### AWS SAM Template. Please replace the parameters where you see <replace-me> text
AWSTemplateFormatVersion: 2010-09-09
Transform:
  - AWS::Serverless-2016-10-31

# Template Information

Description: "Nextjs Website Hosted in S3 and Cloudfront"

# Template Parameters

Parameters:
  DomainName:
    Type: String
    Description: "The domain name of website"
    Default: <replace-me>    # Replace me
  HostedZoneId:
    Type: String
    Description: "The Route53 hosted zone ID used for the domain"
    Default: <replace-me> # Replace me
  AcmCertificateArn:
    Type: String
    Description: "The certificate arn for the domain name provided"
    Default: <replace-me> # Replace me
  IndexDocument:
    Type: String
    Description: "The index document"
    Default: "index.html"
  ErrorDocument:
    Type: String
    Description: "The error document, ignored in SPA mode"
    Default: "404.html"
  RewriteMode:
    Type: String
    Description: "The request rewrite behaviour type"
    Default: "STATIC"
    AllowedValues:
      - STATIC
      - SPA
  CloudFrontPriceClass:
    Type: String
    Description: "The price class for CloudFront distribution"
    Default: "PriceClass_100"
    AllowedValues:
      - PriceClass_100
      - PriceClass_200
      - PriceClass_All


# Resources create conditions

Conditions:
  IsStaticMode: !Equals [!Ref RewriteMode, "STATIC"]
  IsSPAMode: !Equals [!Ref RewriteMode, "SPA"]

# Template Resources

Resources:

  DnsRecord:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneId: !Ref HostedZoneId
      Name: !Ref DomainName
      Type: A
      AliasTarget:
        DNSName: !GetAtt Distribution.DomainName
        HostedZoneId: "Z2FDTNDATAQYW2" # CloudFront
  
  Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref DomainName
      AccessControl: Private
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

  BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref Bucket
      PolicyDocument: 
        Statement: 
          - Effect: "Allow"
            Action: "s3:GetObject"
            Resource: !Sub "arn:aws:s3:::${Bucket}/*"
            Principal: 
              AWS: !Sub 'arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${OriginAccessIdentity}'

  OriginAccessIdentity:
    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: !Ref AWS::StackName

  RewriteRequestStaticFunction:
    Condition: IsStaticMode
    Type: AWS::CloudFront::Function
    Properties: 
      Name: !Sub "${AWS::StackName}-req-static"
      AutoPublish: true
      FunctionCode: !Sub |
        function handler(event) {
          var request = event.request;
          var uri = request.uri
          if (uri.endsWith('/')) {
              request.uri += '${IndexDocument}';
          } else if (!uri.includes('.')) {
              request.uri += '/${IndexDocument}';
          }
          return request;
        }
      FunctionConfig: 
        Comment: !Sub "rewrite all paths to /${IndexDocument}"
        Runtime: cloudfront-js-1.0


  Distribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Enabled: true
        Comment: !Ref AWS::StackName
        DefaultRootObject: !Ref IndexDocument
        HttpVersion: http2
        CustomErrorResponses:
          - ErrorCachingMinTTL: 86400
            ErrorCode: 403 # object not found in bucket, then return 404 status with 404.html file
            ResponseCode: 404
            ResponsePagePath: !Sub "/${ErrorDocument}"
        Origins:
          - DomainName: !Sub "${Bucket}.s3.${AWS::Region}.amazonaws.com"
            Id: bucketOrigin
            S3OriginConfig:
              OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${OriginAccessIdentity}
        DefaultCacheBehavior:
          Compress: true
          AllowedMethods:
            - GET
            - HEAD
            - OPTIONS
          TargetOriginId: bucketOrigin
          ForwardedValues:
            QueryString: false
            Cookies:
              Forward: none
          ViewerProtocolPolicy: redirect-to-https
          FunctionAssociations:
            - EventType: viewer-request
              FunctionARN: !GetAtt RewriteRequestStaticFunction.FunctionMetadata.FunctionARN
              # FunctionARN: !If [IsStaticMode, !GetAtt RewriteRequestStaticFunction.FunctionMetadata.FunctionARN, !GetAtt RewriteRequestSpaFunction.FunctionMetadata.FunctionARN]
        PriceClass: !Ref CloudFrontPriceClass
        Aliases:
          - !Ref DomainName
        ViewerCertificate:
          AcmCertificateArn: !Ref AcmCertificateArn
          SslSupportMethod: sni-only

# Template Outputs

Outputs:
  BucketName:
    Description: "The S3 bucket name where HTML files need to be uploaded"
    Value: !Ref Bucket
  CloudFrontDistribution:
    Description: "The CloudFront distribution in front of the S3 bucket"
    Value: !Ref Distribution
  WebsiteUrl:
    Description: "The website URL"
    Value: !Sub "https://${DomainName}/"

NextJs 13 Static Export Deployment process to host in AWS S3 and cloudfront

With our configuration now complete, the subsequent step is to employ the SAM CLI command to deploy the infrastructure and then transfer the content from the public folder to S3. To accomplish this, we can create a shell script.

npx next build
sam validate
sam deploy --profile aws-profile --region aws-region
aws s3 cp out s3://example.com --recursive --cache-control max-age=31536000 --profile aws-profile --region aws-region

These are the steps which happens when you execute this shell script,

  1. npx next build will generate the out folder with all the statically generated html pages and the assocaited js and css file.
  2. sam validate will validate the sam template yaml and wont proceed with deployment if the template is invalid.
  3. sam deploy will create all the aws resources. In this case it will create S3,cloudfront and cloufront function
  4. aws s3 cp is the last step and this will copy all the contents of the out folder to the s3 bucket. With this command we also set the cache-control headers for the js,css and image files , so that these files are cached in the client browser for subsequent requests and improve the performance.

Cloudfront Function for Redirection

Because of the way in which AWS S3 organizes files, accessing a webpage without the trailing slash may result in a status of 302 being returned, which can harm the SEO. Excerpts from AWS documentation,

If you create a folder structure in your bucket, you must have an index document at each level. In each folder, the index document must have the same name, for example, index.html. When a user specifies a URL that resembles a folder lookup, the presence or absence of a trailing slash determines the behavior of the website. For example, the following URL, with a trailing slash, returns the photos/index.html index document.


http://bucket-name.s3-website.Region.amazonaws.com/photos/
However, if you exclude the trailing slash from the preceding URL, Amazon S3 first looks for an object photos in the bucket. If the photos object is not found, it searches for an index document, photos/index.html. If that document is found, Amazon S3 returns a 302 Found message and points to the photos/ key. For subsequent requests to photos/, Amazon S3 returns photos/index.html. If the index document is not found, Amazon S3 returns an error.

To address this issue, we have a cloudfront function associated with the cloudfront that can manage the redirection and issue the appropriate status code(200 OK in this case) when files are accessed without a trailing slash. For further information on how S3 manages files, please refer to this AWS article.

Sample Repo for NextJs 13 app that can be hosted as a static site in S3

You can access the next 3 static hosted s3 website sample repo here. This sample repo contains a simple nextjs app and also the aws sam template configuration which can be used to create aws infrastructures and deploy the changes to these infrastructure.

Furthermore, you can explore how to host nextjs ssr website using aws lambda and cloudfront in this newly published blog post.Incase you are looking to deploy a nextjs app with the docker,please read our new blog post.