Full working typescript example can be found on my GitHub
The Need
At Voxi we have a multi-domain progressive web app for end user access. We want one application that can have customizeable subdomains as this allows us to only write the application once but customize each experience based on the show. I have been migrating this application from ASP.NET to a static site using React & AWS AppSync. However, when sharing the site via social media, Slack, etc, we need to change the metadata based on URL. In comes AWS Cloudfront server side rendering using Lambda@Edge!
The Struggle
My use case is not exactly supported by AWS. A static site isn’t really meant to change or vary, so many of the headers for caching are unsupported by default when using S3 as the origin. However, you can “trick” Cloudfront by changing your origin from an S3 Origin to a Custom Origin and forcing it to use the Host
header. Unfortunately, when you do this, it breaks the origin
The Solution
There are two main options available when using S3 as the origin:
1. You can have your Lambda@Edge function trigger off of Viewer Request
. This is simplest, but it won’t cache your modified content and will trigger on every request. Example here
1. You can have your Lambda@Edge function trigger off of BOTH Origin Request
and Origin Response
. This is more annoying and requires us to rewrite but does give us caching instead of triggering the function on every user request.
Ultimately I decided to go with option #2 so that I could benefit from some levels of caching.
Modify the site
Modify your site’s index.html
so that it has tokens that we can replace. I went with the style of __OG_TITLE
. Example:
<meta property="og:image" content="__OG_IMAGE__" />
<meta property="ogalt" content="SSR Demo Image" />
<meta property="og:title" content="__OG_TITLE__" />
<meta property="og:description" content="__OG_DESCRIPTION__" />
<meta property="og:url" content="__OG_URL__" />
Code the handler
export const handler = async (event: any = {}): Promise<any> => {
const { config, request } = event.Records[0].cf;
console.log(`Event: ${JSON.stringify(event)}`)
console.log('## Request URI: ', request.uri);
console.log('## Event Type: ', config.eventType);
switch (config.eventType) {
case 'origin-request':
return await processOriginRequest(request);
case 'origin-response':
return await processOriginResponse(event);
default:
return request;
}
}
Process Origin Request
async function processOriginRequest(request: any = {}): Promise<any> {
// Rewrite header
let originalHost = request.headers.host[0].value;
let bucketDomain = request.origin.custom.domainName;
console.log('## Host: ', originalHost);
if (!originalHost) {
console.log('No Host');
return request;
}
const appID = originalHost.split('.')[0];
switch (request.uri) {
case '/index.html':
break;
case '/favicon.ico':
return getFavicon(appID);
default:
if (path.extname(request.uri) !== "") {
request.headers['x-forwarded-host'] = [{ key: 'X-Forwarded-Host', value: originalHost }];
request.headers.host = [{ key: 'Host', value: bucketDomain }];
return request;
}
}
try {
const siteURL = `http://${bucketDomain}/index.html`;
console.log('## SITE URL: ', siteURL);
const indexResult = await axios.get(siteURL);
let index = indexResult.data;
const metas = await getMetadata(appID);
for (const meta in metas) {
if (metas.hasOwnProperty(meta)) {
index = index.replace(new RegExp(`__${meta}__`, 'g'), metas[meta]);
}
}
return {
status: 200,
statusDescription: 'HTTP OK',
body: index
}
} catch (err) {
console.log('### ERROR ###');
console.log(JSON.stringify(err));
}
return request;
}
Process Origin Response
async function processOriginResponse(event: any = {}): Promise<any> {
const { request, response } = event.Records[0].cf;
// Do anything you need to do, if anything
return response;
}
Deploy the solution!
Next is to deploy the solution - GitHub. This is a fully working CDK example, with the edge function deployed in us-east-1 and the rest deployed into us-east-2 to demonstrate cross region deployments.
- Deploy the Lambda@Edge stack - named
EdgeLambdaFunctionStack
- Deploy the Cloudfront Distribution stack - named
CloudfrontSsrStack
Live Version Available
- https://ssrdemo.advocacy.kennethwinner.com
- https://ssrdemo2.advocacy.kennethwinner.com
- https://other.advocacy.kennethwinner.com
Social Share Results
Slack Share Results
References
- https://github.com/kcwinner/advocacy/tree/master/cloudfront-ssr
- https://stackoverflow.com/questions/34882125/getting-403-forbidden-when-loading-aws-cloudfront-file
- https://medium.com/@lucienperouze/how-to-layout-facebook-shares-of-a-react-web-app-hosted-on-aws-s3-cloudfront-1ee146dbb5d2
- https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-cloudfront-trigger-events.html
- https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-updating-http-responses.html