Skip to content

Instantly share code, notes, and snippets.

@alexcasalboni
Last active July 25, 2024 22:45
Show Gist options
  • Save alexcasalboni/046471a7dcfbb6c468f843361c474aff to your computer and use it in GitHub Desktop.
Save alexcasalboni/046471a7dcfbb6c468f843361c474aff to your computer and use it in GitHub Desktop.
Serve dynamically generated, minimized and compressed HTML pages with AWS Lambda@Edge.

AWS Lambda@Edge Experiment

Requirements

  • AWS Lambda@Edge (enabled Preview)
  • One Amazon CloudFront Distribution (origin doesn't matter)
  • IAM role (basic execution is enough)
  • npm to install Node.js dependencies

Lambda Function Details

The Lambda@Edge will be invoked whenever a new "Viewer Request" event is triggered by CloudFront.

The Lambda Function will behave as follows:

  1. If the requested resource is NOT available locally (i.e. not an HTML file), the request can proceed to the origin
  2. If the local template exists, it will be read and rendered using Plates with a few dynamic variables (i.e. "title" and "today)
  3. The resulting HTML is then minified and eventually compressed, based on the request HTTP headers (response headers are correctly set as well)
  4. The final HTTP body is directly returned to the client without hitting the CloudFront origin

How to create the Deployment Package

cd this-gist

npm install

zip -r ../edge-deployment-package.zip ./*

Known Limitations

  • The deployment package cannot exceed 1MB, and a manual hack was required to include the 'html-minifier' library (i.e. reducing its size from 3MB to 500KB)
'use strict';
const fs = require('fs');
const zlib = require('zlib');
const Plates = require('plates');
const minify = require('html-minifier').minify;
const supportedCompression = ['gzip', 'deflate'];
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
// read local file
fs.readFile('/hook/unzipped' + request.uri.split('?')[0], function (err, data) {
if (err) {
callback(null, request); // bypass Lambda@Edge
} else {
renderHTML(data.toString(), request, callback);
}
});
};
function renderHTML(html, request, callback) {
const data = {
"title": "This is a Lambda@Edge test!",
"today": new Date().toString(),
};
// generate body and bind variables
const body = minifyHTML(Plates.bind(html, data));
// detect compression from request headers
const compression = detectCompression(request);
const response = {
status: '200',
statusDescription: 'HTTP OK',
httpVersion: request.httpVersion,
body: compressBody(body, compression),
headers: {
'Vary': ['*'],
'Content-Type': ['text/html; charset=UTF-8'],
'Last-Modified': ['2017-02-09'],
'Content-Encoding': [compression || 'UTF-8']
},
};
callback(null, response); // return custom response
}
function minifyHTML(html) {
return minify(html, {
collapseWhitespace: true,
removeComments: true,
});
}
function detectCompression(request) {
const accept = request.headers['Accept-Encoding'] || [];
for(var i = 0; i < accept.length; i++) {
if (supportedCompression.indexOf(accept[i]) !== -1) {
return accept[i]; // return the first match
}
}
return null;
}
function compressBody(body, compression) {
if (compression === 'gzip') {
return zlib.gzipSync(body).toString('utf8');
} else if (compression === 'deflate') {
return zlib.deflateSync(body).toString('utf8');
} else {
return body; // no compression
}
}
{
"name": "gulp-htmlmin",
"description": "AWS Lambda@Edge Experiment",
"version": "0.0.1",
"author": {
"name": "Alex Casalboni",
"url": "https://github.com/alexcasalboni/"
},
"dependencies": {
"plates": "0.4.11",
"html-minifier": "3.3.1"
}
}
<html>
<head>
<title>Test</title>
</head>
<body>
<h1 id="title"></h1>
<p id="today"></p>
</body>
</html>
@alexcasalboni
Copy link
Author

It looks like you have to compress the response yourself, if you are generating a dynamic reponse on 'Viewer Request'.

You can find out more at the HTTP Response with Compressed Static Content section here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment