Introduction
Recently, I undertook the task of rebuilding my personal website using Next.js. My goal was to build a basic static blog using next.js and host it on AWS Cloudfront and S3. Despite finding several tutorials, none seemed to fit my specific requirements, leading me to experiment through trial and error. In this blog post, I aim to share the effective solution I discovered, hoping to streamline this process for others.
Prerequisites
Before diving into the details, here are the prerequisites:
- A Next.js application ready for deployment.
- An active AWS account with necessary permissions.
Preparing Your Next.js Application
The first step involves preparing your Next.js application for deployment. Ensure your application is production-ready by running:
npm run build
and you have next.config.js configured in export mode
const nextConfig = {
output: 'export',
// other configs
}
This command generates a out
folder containing optimized production builds of your application.
Issues with Next.js and AWS Cloudfront
While setting up my Next.js application on AWS Cloudfront using a modified dev-kit static build template, I encountered a significant challenge with path routing. This issue arises from the mismatch between the URL path on the website (like site.com/contact
) and the corresponding file in the S3 bucket (contact.html
).
Path Routing Problem
In a typical Next.js setup, navigating to site.com/contact
expects a contact.html
at the root. Most web servers can handle this by serving the contact.html
file when the request URI is /contact
.
However, s3 is not a web server, and only understands key paths.
Using Next.js's trailingSlash
option creates nested files like contact/index.html
, but this still doesn't solve the problem due to the .html
extension mismatch.
Solution: Cloudfront Edge Function
To overcome this, I implemented a Cloudfront edge function to rewrite incoming request URIs. The function modifies the incoming URI to point to the correct HTML file in the S3 bucket. Below is the TypeScript code for the edge function:
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;
};
Integration with AWS CDK
For those using the AWS Cloud Development Kit (CDK), integrating this solution is straightforward. Below is a CDK stack example that includes the edge function:
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);
}
}
In this CDK stack, we define a Lambda@Edge function that rewrites the request URI. This ensures that the path routing issue is resolved, making the Next.js application function correctly when hosted on AWS Cloudfront.
By implementing these solutions, I was able to successfully deploy my Next.js application on AWS Cloudfront and overcome the routing challenges. This approach can be beneficial for anyone facing similar issues with Next.js and AWS Cloudfront integration.