Nuxt 3 Static Website Hosting in AWS S3 and Cloudfront

aws s3

Looking to host your Nuxt 3 static website on AWS S3 and CloudFront? Our comprehensive guide walks you through the process step-by-step, helping you achieve lightning-fast performance and optimal scalability.


The meta framework Nuxt 3, which is based on Vue 3, provides a range of deployment configurations to choose from. For those developing a static site and seeking an affordable hosting solution, AWS S3 is currently the optimal choice. Nuxt implemented full static features in version 2, which have also been carried over to Nuxt 3. This blog post details how to create a Nuxt 3 website for full static deployment, deploy the resulting artifacts to AWS S3, and leverage AWS CloudFront to achieve maximum performance and edge caching. This blog is written with the following versions:

  • Nuxt 3.3.1
  • Node 18
  • AWS SAM CLI 1.76

AWS S3 and cloudfront architecture for Nuxtjs static website

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

Nuxt 3 S3 Deployment Architecture

In this setup, we will employ a single AWS S3 bucket to host all the necessary files, which includes JavaScript, CSS, images, and the index.html files that are generated during the nuxt generate process. To enable the static website hosting feature on this bucket and allow cloudfront to access the content from the bucket using origin policy, we will need to make some modifications. Additionally, we will utilize AWS CloudFront to sit in front of the S3 bucket, which will help with edge caching of the files. By caching the files on CloudFront, we can limit read calls to S3 and enhance the performance and security of our website.

Nuxt Build process for Full static generation

The static artifacts can be generated using nuxt generate and the artifacts are generated in the .output/public.

  • nuxt3 output

The folder includes all the static contents like html,images, js and css files. The index.html under the public folder is the home page index file and for each route there will be a folder and a corresponding index.html file. The entire content of the public folder will be copied to the AWS s3 and it will be hosted there. For the js,css and image files, we should set the cache control to 1year , so that these files can be cached on the browser.

AWS S3 and Cloudfront Infrastructure 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: "Nuxt 3 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}/"

Nuxt 3 Deployment process for AWS S3 and cloudfront

Now we have all the configuration ready, next step is to execute the sam cli command for deployment of the infrastructure and then copying the content from public folder to s3. Lets achieve all these using a shell script.

npx nuxt generate
sam validate
sam deploy --profile aws-profile --region aws-region
aws s3 cp .output/public/ 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 nuxt generate will generate the .output 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 .output 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 Nuxt 3 web that can be hosted as a static site in S3

You can access the nuxt 3 static hosted s3 website sample repo here. This sample repo contain a Nuxt 3 web app with multiple routes and AWS SAM template which can be used for hosting the file. Only requirement is to have the right permissions before executing this AWS SAM CLI command to create all the AWS services.