One big advantage of static website is that they are, well, static. This means they don't change between users. The website is rendered differently on the client, depending on the users input, but the downloaded files are the same for every user. This allows for efficient hosting when using the correct caching strategies (multiple users download the same file, but the file only has to be requested from the host once).
When hosting static websites in CloudFront, you use a Lambda@edge function to link requests (paths) to S3 origin locations (origin request) and a Lambda@edge function to add or modify headers in the response (origin response).
We can use the latter to add the Cache-Control
header to the response. This header tells the browser to cache the file for the time we set. CloudFront also reads this header and uses it to determine its caching time for. So we have a double benefit:
S3 and CloudFront are pretty stupid. They don't know that when a request for /blog
is received, the browser might mean /blog/index.html
. We have to do that ourselves with an origin request function.
This function will translate/rewrite requests so S3 know which file to return to CloudFront. While we're doing rewrites, we might as well use this function to do redirects too.
I moved my blog from the main domain www.joeplaa.com
to a subdomain blog.joeplaa.com
. I know which paths to redirect so I listed them in an array. If a request comes in that contains a /
and is in the redirects
array, a redirect response is created.
// Attached to ORIGIN REQUEST
// https://www.ximedes.com/2018-04-23/deploying-gatsby-on-s3-and-cloudfront/
// For event object example: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-event-structure.html
const redirects = [
'the-news',
'our-emotional-seesaw',
'whiteness-defined',
'low-salt-intake-is-bad',
'newspeak',
'rule-2-treat-yourself-like-someone-you-are-responsible-for-helping',
'not-a-political-journey-too',
'living-under-autism',
'im-on-edge',
'document',
'what-i-learned-from-migrating-our-websites',
'tearing-down-the-house',
'lessons-from-my-running-failure',
'learning-to-code',
'do-i-have-a-sugar-intolerance',
'customer-service',
'keep-asking-why-a-letter-to-my-niece-and-nephew',
'intermittent-fasting',
'my-priorities',
'my-favorite-quotes',
'whats-the-right-diet-for-me',
'the-struggle-for-perfection',
'the-ultimate-why-improve-things',
'what-i-learned-this-year',
'my-goals-a-little-more-context-part-2',
'my-goals-a-little-more-context-part-1',
'joeps-goals',
'running-for-me',
'why-do-most-parties-start-around-midnight',
'please-teach-me',
'i-love-the-beach'
];
exports.handler = (event, context, callback) => {
const { request } = event.Records[0].cf;
const { uri } = request;
const mainDomain = 'joeplaa.com';
const redirectDomain = `blog.${mainDomain}`;
const baseURI = `https://${redirectDomain}`;
// Check for blog redirects first
if (redirects.includes(uri.replace(/\//g, ''))) {
const redirectResponse = {
status: '301',
statusDescription: 'Moved permanently',
headers: {
location: [{
key: 'Location',
value: `${baseURI}${uri}`
}],
'cache-control': [{
key: 'Cache-Control',
value: 'max-age=86400' // 60 * 60 * 24
}]
}
};
callback(null, redirectResponse);
return;
}
// If no "." in URI, assume document request and append index.html to request.uri
if (request.uri.match(/^[^.]*$/)) {
if (request.uri[request.uri.length - 1] === '/') {
request.uri += 'index.html';
} else {
request.uri += '/index.html';
}
}
// Return to CloudFront Origin Request
callback(null, request);
};
At jodiBooks we use three static site generators or frameworks: Gatbsy.js, Next.js and Docusaurus v2. All of them serialize the static files (images, scripts, stylesheets, downloads) or put them in a serialized folder. Unfortunately each framework uses its own method of grouping the static files and it is quite difficult (or impossible) to find the exact specifics in their docs. So you have to figure this out yourself. I'll save you some trouble with the table below, which lists the cacheable resources I identified.
File types | Docusaurus | Gatbsy.js | Next.js |
---|---|---|---|
Scripts (.js , .js.map ) |
*.js (in main folder only) |
*.js , *.js.map (everywhere) |
/_next/static |
Stylesheets (.css ) |
*.css (in main folder only) |
*.css (everywhere) |
/_next/static |
Images (.png , .jpg , .webp , .gif ) |
/assets/images |
/static |
/_next/static |
Downloads/asset (.pdf , .zip ) |
/assets/files |
/assets (when manually versioned) |
/_next/static |
Excluded folders (non-serialized) | all other folders (/css , /js specifically) |
all other folders | all other folders |
We want to cache as many of the serialized files as possible and as each framework has a slightly different strategy we have to either create 3 separate Lambda functions or write a more complicated one with some conditional logic.
Depending on your framework, you can take one of the JavaScript functions from this Github gist and connect them to the origin response
of your CloudFront distribution. Also included are jest
tests to check if indeed the findings in the table above are indeed honored.
If you feel adventurous or don't want to have multiple functions, basically performing the same task, you can also combine them into one. I made one assumption here (as that applied to our use case): all the downloads (assets) in the Gatsby.js site are manually versioned.
// Attached to: ORIGIN RESPONSE
// https://www.ximedes.com/2018-04-23/deploying-gatsby-on-s3-and-cloudfront/
// For event object example: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-event-structure.html
const cacheableFolders = [
'/assets/',
'/static/',
'/_next/static/'
];
// Docusaurus: main folder only!
const cacheableExtensions = [
'.css',
'.js',
'js.map',
];
// Docusaurus: excluded folders!
const excludedFolders = [
'/css/',
'/js/'
];
const headerCacheControl = 'Cache-Control';
const headerCacheControlLC = headerCacheControl.toLowerCase();
const defaultTimeToLive = 60 * 60 * 24 * 365; // 365 days in seconds
const noCacheResponseHeader = {
key: headerCacheControl,
value: `public, max-age=0, must-revalidate`,
};
const cacheResponseHeader = {
key: headerCacheControl,
value: `public, max-age=${defaultTimeToLive}, immutable`,
};
exports.handler = (event, context, callback) => {
// Extract the request and response from the Cloudfront Origin Response event
const { request, response } = event.Records[0].cf;
const { headers } = response;
if (response.status === '200') {
if (!headers[headerCacheControlLC]) {
// Cache file if it is within one of 'cacheableFolders'
if (cacheableFolders.some(folder => request.uri.includes(folder))) {
headers[headerCacheControlLC] = [cacheResponseHeader];
// Or if filetype is one of 'cacheableExtensions' (use 'endsWith()' to exclude 'json'; using 'includes()' also includes 'json')
} else if (cacheableExtensions.some(extension => request.uri.endsWith(extension))) {
// if request is to an excluded folder do not cache
if (excludedFolders.some(folder => request.uri.includes(folder))) {
headers[headerCacheControlLC] = [noCacheResponseHeader];
}
// Otherwise (not in an excluded folder) do cache requested file
else {
headers[headerCacheControlLC] = [cacheResponseHeader];
}
}
// Otherwise (not in a cacheable folder) do not cache requested file
else {
headers[headerCacheControlLC] = [noCacheResponseHeader];
}
}
}
// Return to Cloudfront Origin Response event
callback(null, response);
};
Click "Functions" and "Create function"
CloudFrontS3Webserver
Node.js 14.x
CloudFrontS3Webserver
Basic Lambda@Edge permissions (for CloudFront trigger)
CloudFront
and click "Deploy to Lambda@Edge".
Origin request
Click "Functions" and "Create function"
CloudFrontS3UpdateHeaders
Node.js 14.x
CloudFrontS3UpdateHeaders
Basic Lambda@Edge permissions (for CloudFront trigger)
CloudFront
and click "Deploy to Lambda@Edge".
Origin response
Go to CloudFront console.
Click "Policies" in the menu and click "Create cache policy"
Enter a name: StaticWebsiteCaching
and under TTL settings set the "Minimum TTL" to 0
. You can leave the others at their default values. Enable both "GZIP" and "Brotli" for Compression support.
Save the policy.
Go to "Distributions" and select your website. Go to the "Behaviors" tab and select the default behavior.
Click "Edit" and scroll down to Cache key and origin requests
Set the Cache policy to StaticWebsiteCaching
and the Origin request policy to CORS-S3Origin
.
Save the changes and wait for the CloudFront distribution to update.
Sources:
https://www.bayphillips.com/blog/gatsbyjs-using-amazon-s3-and-cloudfront/
https://www.ximedes.com/2018-04-23/deploying-gatsby-on-s3-and-cloudfront/
https://docs.aws.amazon.com/lambda/latest/dg/lambda-edge.html
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-cloudfront-trigger-events.html (image source)