↓Skip to main content
  1. Articles/

Host Hugo on S3 + CloudFront

··4 mins

This site uses Hugo, with the Congo theme.
I won’t bother you on how to use Hugo, write articles in markdown format, or how to configure your theme; there are already good articles online (Congo theme documentation was a good start for me, and in a day, everything was online)

Why CloudFront + S3 #

The key for me is simplicity and using what I know best. This side blog is a personal learning project, but I don’t want to focus on the tools I use to write. I need to get the job done and have time to learn other things and write articles, so here it is: the basic exercise you do when you approach AWS Cloud Managed services is really something you can do.
For Hugo, you need to pay attention to a peculiar feature of CloudFront and S3: the default page (index.html) is valid only for the root of your website, not for the “subfolders” (do you know that the concept of folder does not exist in S3, right? If you don’t know, S3 has a flat namespace). So, if you deploy your website after configuring these two services, you’ll find out that only the homepage works, and if you click on an article in a subfolder, you will get the dreaded “NoSuchKey” error.
We will see how to fix this issue with CloudFront functions later.

Deploy CloudFront and S3 #

You can use the solution from AWS for a quick deploy, or follow this tutorial if you are not familiar with CloudFormation and you want to learn how to use the AWS Console

A note if you start today: the AWS solution now configures Origin Access Control (OAC), so the bucket stays private and the website is reachable only through CloudFront, as AWS recommends. The Medium tutorial is an older console walkthrough (2023) that uses the S3 website endpoint as the CloudFront origin, and this pattern can’t use OAC, because OAC works only with the bucket’s REST endpoint. For a new setup, I would start from the AWS template.

Once you have your website configured, we will need to add a tiny part.

Configure the CloudFront function to make Hugo subdirectories work #

CloudFront doesn’t behave as Apache or Nginx, so it will search for index.html only on the “root” of your website, giving a “NoSuchKey” error (or “AccessDenied”, if your bucket is private) if you try to access a subdirectory (like /blog/categories).
Lucky for us, CloudFront Functions exist and can help. AWS already has a repository for standard use cases.
Pro tip: explore the aws-samples repositories full of hidden gems.
We will use the URL rewrite to append index.html to the URI for single page applications example.
Here’s the code, for simplicity:

async function handler(event) {
    var request = event.request;
    var uri = request.uri;
    
    // Check whether the URI is missing a file name.
    if (uri.endsWith('/')) {
        request.uri += 'index.html';
    } 
    // Check whether the URI is missing a file extension.
    else if (!uri.includes('.')) {
        request.uri += '/index.html';
    }

    return request;
}

Create the CloudFront functions #

  1. Go to the CloudFront console, select “CloudFront functions” from the left menu.
    CloudFront Menu
    .
  2. Create the function, give it a name, a description, and select 2.0 as the JS version.
    CloudFront Menu
  3. Paste the function code.
    CloudFront Function Code
  4. Publish the code
    CloudFront Function Publish Code

Associate the CloudFront Function with your website #

  1. On the CloudFront console, select your distribution and click on the behaviors tab
    CloudFront behaviors tab
  2. Select the “Default” (it should be the only one), click “Edit”, and go to the bottom of the page. Select the “Viewer request”, select “CloudFront function” from the dropdown menu, select your function
    Select CloudFront Function on Viewer Request

You’re all set! Happy blogging with Hugo!

Wait, and how do I deploy?

Deployment #

You can use a pipeline, like GitHub Actions or CodePipeline. When I first wrote this article, I preferred the “old and dirty” method: sync files. Since I don’t write every day, I didn’t feel the urge to have an automatic deployment mechanism, so to update my website, I ran:

hugo
aws s3 sync --delete public/ s3://YOUR_BUCKET/

This will sync (a la rsync) my content to the S3 Bucket, deleting old assets, and I’m all set! Lazy, but it works!

After some time, I became too lazy also for that, and I moved the same publish step into GitHub Actions, using OIDC and no long-lived AWS access keys. Keep in mind that, after a sync, CloudFront can still serve the old version from its cache, and the new article also covers the invalidation I use now. You can find everything in Automate Hugo deploys to S3 and CloudFront with GitHub Actions OIDC.

Damiano Giorgi
Author
Damiano Giorgi
Ex on-prem systems engineer, lazy and prone to automating boring tasks. In constant search of technological innovations and new exciting things to experience. And that’s why I love Cloud Computing! At this moment, the only ‘hardware I regularly dedicate myself to is that my bass; if you can’t find me in the office or in the band room try at the pub or at some airport, then!

comments powered by Disqus