hotmeatballsoup
hotmeatballsoup

Reputation: 605

Building and serving React/Nextjs from AWS S3

I am building a React/Nextjs app and plan on pushing it to AWS S3 and serving it from AWS CloudFront (using the S3 bucket as a distribution origin). I will be using Route53 and DNS configurations to map my domain (say, https://myapp.example.com) to the CF distribution. But I can take care of that.

In the documentation I see that I can build and run Nextjs apps in "production mode" via:

npm run build
npm run start

However here, I want to take everything that is generated by npm run build (possibly the contents of the .next/ directory??) and upload that output to my S3 bucket.

And obviously I'm hoping that will be sufficient so that when a user goes to my domain, and they get pointed to the CF distribution (and subsequently the S3 bucket backing that distribution) they download the fully built + transpiled app and it loads & runs in their browser.

How can I accomplish this? What needs to be stored on S3? And are there any special configurations that need to be supplied so that it runs in the browser as soon as they fetch the built/transpiled app from S3?

Upvotes: 2

Views: 2997

Answers (4)

Ismoil Shokirov
Ismoil Shokirov

Reputation: 3011

An addition to the answer of @billias, today there is no next export command.

The "next export" command has been removed in favor of "output: export" in next.config.js. Learn more: https://nextjs.org/docs/advanced-features/static-html-export

/**
 * @type {import('next').NextConfig}
 */
const nextConfig = {
  output: 'export',
 
  // Optional: Change links `/me` -> `/me/` and emit `/me.html` -> `/me/index.html`
  // trailingSlash: true,
 
  // Optional: Prevent automatic `/me` -> `/me/`, instead preserve `href`
  // skipTrailingSlashRedirect: true,
 
  // Optional: Change the output directory `out` -> `dist`
  // distDir: 'dist',
}
 
module.exports = nextConfig

Upvotes: 1

Cole Murray
Cole Murray

Reputation: 593

I recently went through the pain of setting this up. I experienced issues with the following:

Path routing
There is a routing issue due to a mismatch in pathing "site.com/contact" and the files in s3, contact.html. You'll encounter various hacks from using Nextjs' trailingSlash option, which will create nested files contact/index.html, but still has an issue due to the .html mismatch.

To resolve this, I added a cloudfront edge function to rewrite incoming requests uri.

exports.handler = async (event) => {
    const request = event.Records[0].cf.request;
    const uri = request.uri;

    
  if (uri === '/') {
    // turns "/" to "/index.html"
    request.uri += 'index.html'
  } else if (uri.endsWith('/')) {
    // turns "/foo/" to "/foo.html"
    request.uri = uri.slice(0, -1) + '.html'
  } else if (!uri.includes('.')) {
    // turns "/foo" to "/foo.html"
    request.uri += '.html'
  }

  return request;
};

If using CDK, you can use this stack:

import {Certificate} from 'aws-cdk-lib/aws-certificatemanager';
import {
    CloudFrontWebDistribution,
    CloudFrontWebDistributionProps,
    OriginAccessIdentity,
    ViewerCertificate,
} from 'aws-cdk-lib/aws-cloudfront';
import {BlockPublicAccess, Bucket} from 'aws-cdk-lib/aws-s3';
import {BucketDeployment, ISource} from 'aws-cdk-lib/aws-s3-deployment';
import {Construct} from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import {Runtime, Code} from 'aws-cdk-lib/aws-lambda';
import {LambdaEdgeEventType} from 'aws-cdk-lib/aws-cloudfront';


export interface StaticSiteProps {
    readonly certificateArn?: string;
    readonly domainNames?: string[];
    readonly sourceAsset: ISource;
}

export class StaticSiteWithRewrite extends Construct {
    constructor(scope: Construct, id: string, props: StaticSiteProps) {
        super(scope, id);

        if ((props.certificateArn && !props.domainNames) || (!props.certificateArn && props.domainNames)) {
            throw new Error('Both certificateArn and domainNames must be provided if one is provided.');
        }

        // Lambda@Edge Function
        const edgeLambda = new lambda.Function(this, `${id}EdgeLambda`, {
            runtime: Runtime.NODEJS_LATEST,
            handler: 'index.handler',
            code: Code.fromInline(`
exports.handler = async (event) => {
    const request = event.Records[0].cf.request;
    const uri = request.uri;


    if (uri === '/') {
        // turns "/" to "/index.html"
        request.uri += 'index.html'
    } else if (uri.endsWith('/')) {
        // turns "/foo/" to "/foo.html"
        request.uri = uri.slice(0, -1) + '.html'
    } else if (!uri.includes('.')) {
        // turns "/foo" to "/foo.html"
        request.uri += '.html'
    }

    return request;
};
      `),
        });

        const bucket = new Bucket(this, `${id}WebsiteBucket`, {
            websiteIndexDocument: 'index.html',
            blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
        });

        const originAccessIdentity = new OriginAccessIdentity(this, 'OAI');
        bucket.grantRead(originAccessIdentity);

        new BucketDeployment(this, `${id}DeployWebsite`, {
            sources: [props.sourceAsset],
            destinationBucket: bucket,
        });

        let distributionProps: CloudFrontWebDistributionProps = {
            originConfigs: [
                {
                    s3OriginSource: {
                        s3BucketSource: bucket,
                        originAccessIdentity: originAccessIdentity,
                    },
                    behaviors: [
                        {
                            isDefaultBehavior: true,
                            lambdaFunctionAssociations: [{
                                eventType: LambdaEdgeEventType.ORIGIN_REQUEST,
                                lambdaFunction: edgeLambda.currentVersion,
                            }],
                        }
                    ],
                },
            ],
        };

        if (props.certificateArn && props.domainNames) {
            distributionProps = {
                ...distributionProps,
                viewerCertificate: ViewerCertificate.fromAcmCertificate(
                    Certificate.fromCertificateArn(this, `${id}certificate`, props.certificateArn),
                    {aliases: props.domainNames},
                ),
            };
        }

        new CloudFrontWebDistribution(this, `${id}WebsiteDistribution`, distributionProps);
    }
}

Upvotes: 1

billias
billias

Reputation: 825

First, you need to run an extra command to generate the static files from next, after building.

next export

This command will create a directory out. The contents of this directory should be uploaded to your S3 bucket.

Then, you need to enable "Static website hosting" on your S3 bucket through AWS console (enable it via bucket properties). For index and error documents add index.html (or whatever your main html file is).

You also need to allow access to the S3 files using the following permissions

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::[your-bucket-name]/*"
        }
    ]
}

Upvotes: 5

Ranu Vijay
Ranu Vijay

Reputation: 1257

You will need a reverse proxy service that match request url with you website url and then return the response. Read about Nginx or similar aws related services.

If you only run build and start npm command you will need to have a node server. You can export and this will be a complete static website, can run without node.

Upvotes: 0

Related Questions