Generating and Serving Static HTML Page with AWS CloudFront

For my latest project, I had to serve an HTML page with the poem generated by my step function. Since all of my functionality for the project was created inside the AWS ecosystem I've naturally chosen CloudFront to perform this task. While completing this task, I've faced a couple of challenges. Below, I'll explain them and show how I've solved them.

Preparing the data

My system consists of multiple Lambdas coordinated by a step function. HTML page generation happens inside the reducer function, and you may think of it... well as a reducer inside a distributed map-reduce job. Reducer function accepts input from multiple mappers. Each mapper returns an array of objects representing sentences processed by the sentiment analysis ML model. You can imagine type shape of such object as below.

interface MLProcessedObject {
  Text: string
  SentimentValue: number
  Magnitude: number
}

Given that a single function returns an array of such objects, the reducer function accepts an array of arrays. So, in order to extract a limited number of sentences with the strongest sentiment for our poem, we need to flatten the array, sort it by sentiment value in a descending way, and extract the top sentences. In terms of the code it looks as below.

const extractMostColorfulSentences = (data, sentenceCount) => {
    return data.flat()
        .filter(_ => _ != null)
        .sort((a, b) =>
            calculateSentiment(b.SentimentValue, b.Magnitude) - calculateSentiment(a.SentimentValue, a.Magnitude))
        .slice(0, sentenceCount)
        .map(item => item.Text)
}

Now, let's unwrap how we calculate sentiment. We cannot take into account solely SentimentValue because sentiment is encoded by both SentimentValue and Magnitude. Instead, we'll use the following code.

const calculateSentiment = (value, magnitude) => {
    return value * Math.pow(10, magnitude);
}

Now, let's write some unit tests to make sure the logic is correct. For unit testing, we'll use mocha and expect.js

/*global describe, it*/
import expect from 'expect.js'

import extractMostColorfulSentences from '../reducer/poem-selector.mjs'

describe('extractMostColorfulSentences', () => {
    it('should return sentence text based on magnitude and sentiment', () => {
        const data = [
            [{
                "Text": "The dog is on the table.",
                "SentimentValue": 0.9,
                "Magnitude": 0.1 //1.13
            },
            {
                "Text": "The cat is on the table.",
                "SentimentValue": 0.7,
                "Magnitude": 0.2 //1.109
            }],
            [{
                "Text": "The rat is on the table.",
                "SentimentValue": 0.5,
                "Magnitude": 0.3 //0.99
            },
            {
                "Text": "The mouse is on the table.",
                "SentimentValue": 0.3,
                "Magnitude": 0.4 //0.75
            }],
            [{
                "Text": "The elephant is on the table.",
                "SentimentValue": 0.5,
                "Magnitude": 0.5 //1.26
            }]
        ]
        const sentenceCount = 2
        const expected = [
            "The elephant is on the table.",
            "The dog is on the table."
        ]
        const actual = extractMostColorfulSentences(data, sentenceCount)
        expect(actual).to.eql(expected)
    })
})

Creating HTML page

When creating the HTML challenge I had to provide a consistent look and feel with the rest of my site. Also, I had to do this every week since the poem rewrites itself. The solution was to come up with some sort of HTML template that would be filled with the actual poem. When it comes to template languages, there are two solutions in a Javascript ecosystem: mustache and handlebars. While handlebars are more powerful in terms of functionality, I've decided to stick with the mustache due to its minimalistic nature and the fact that it covers all my needs (which are quite basic).

So, I've defined the template in a static HTML file.

<html>
    <head>
        <title>Bohdan Stupak</title>
        <link href="http://tilde.town/~wkalmar/style.css" rel="stylesheet" />
    </head>
    <body>
		<article>
{{{poem}}}
		</article>
	</body>
</html>

The point of interest in the template is that we have to use triple curly brackets for the inserted HTML not to be escaped.

Now, I pass my poem inside the template and render it.

const formatted =
        poem
          .map(p => `<p>${p}</p>`)
          .join("\n");
const html = renderTemplate(formatted);

const renderTemplate = (poem) => {
  const template = fs.readFileSync('./template.html', 'utf8');

  return Mustache.render(template, {
    poem: poem
  });
}

Saving output to S3

To serve the HTML page as a CloudFront website, you have to point CloudFront to the S3 bucket. The complete process is exhaustively described in the docs. However, we'll briefly cover the process of generating an S3 document from the template we've rendered in a previous section.

import aws from 'aws-sdk'
const s3 = new aws.S3();
const putParams = {
  Bucket: 'poeme-concrete',
  Key: 'index.html',
  Body: html,
  ContentType: 'text/html',
};

await s3.putObject(putParams).promise();

Here, we use AWS SDK to get programmatic access to the S3 bucket. We've provided put parameters such as bucket, content type, our rendered template, and filename inside the bucket. With these parameters, the process of updating a file is as simple as a single call to SDK function.


Similar Articles