9 minutes
Using CloudFront Functions to migrate ugly URLs to pretty URLs seamlessly

Context and Problem Statement
I started this blog in 2021, (it’s been a while!) if you’ve read my previous blog posts about this here and here I mentioned there that I decided to host my Hugo-powered blog in AWS using CloudFront and S3 because it’s cheap and is very low-maintenance but unfortunately there is a downside - I cannot enable Pretty URLs out of the box.
Why do I need to enable pretty URLs anyway? Despite being a humble blog, I’ve decided to implement Pretty URLs for the sake of curiosity and to be a considerate internet citizen. While it may seem like overkill, I believe there are valid reasons to pursue this endeavour.
Firstly, Pretty URLs enhance the user experience by making the website more inviting and easier to navigate. With clean and descriptive URLs, readers can understand the content of a page just by looking at the URL itself. It adds a level of professionalism and helps users find what they’re looking for effortlessly.
Secondly, Pretty URLs can have a positive impact on search engine optimization (SEO). Search engines prefer URLs that are clear, relevant, and contain keywords related to the page’s content. By using descriptive keywords in the URLs, my blog has a better chance of appearing in search results and attracting organic traffic.
Thirdly, Pretty URLs are more shareable and can be easily communicated or shared with others. They look cleaner, more professional, and more memorable, encouraging others to link to my content and increase its visibility.
Moreover, implementing Pretty URLs aligns with consistency and branding. It creates a cohesive and recognizable structure throughout my blog, making it easier for readers to remember and navigate specific pages.
Lastly, by adopting Pretty URLs, I future-proof my blog. It allows me to reorganize content, implement redirects, or make structural changes without negatively impacting the user experience or search engine visibility.
Hugo uses Pretty URLs by default but it doesn’t work when hosting Hugo-generated websites on CloudFront and S3, the issue lies in the way CloudFront and S3 handle routing and file retrieval. S3 is an object storage service that does not natively support complex routing or URL rewriting. By default, S3 treats each object (file) as a separate entity and does not have built-in support for handling pretty URLs without file extensions. CloudFront, on the other hand, acts as a content delivery network, it caches the responses from the origin (S3 in this case) to improve performance and reduce the load on the origin server. When CloudFront caches the responses, it may not recognize or handle the routing for pretty URLs correctly, as it expects to map specific files or patterns.
S3 as a Static Website?
I’m sure most of you are wondering why not just enable static website hosting on my S3 Bucket? Enabling static website hosting on an S3 bucket may seem like a straightforward option for hosting a Hugo-generated site with Pretty URLs enabled, but there are some considerations to keep in mind. One of the main challenges is the potential for circumvention of CloudFront and direct access to the S3 bucket.
By enabling static website hosting on an S3 bucket, the bucket is required to be publicly accessible. This means that anyone can access the content directly from the S3 bucket URL, bypassing the CloudFront distribution. This can undermine the benefits of using CloudFront, such as caching, edge optimization, and improved performance.
To address this issue, one option is to restrict access to the S3 bucket by using custom headers on CloudFront. By configuring CloudFront to send custom headers to the S3 bucket, I can set up the origin (S3 static website) to require these headers for access. This helps ensure that only requests coming through my CloudFront distribution with the specified custom headers can access my S3 bucket.
However, this solution has a limitation. While it restricts direct access to the S3 bucket, it doesn’t prevent someone from creating their own CloudFront distribution and passing the same custom headers to access the S3 bucket. In other words, someone can potentially bypass my CloudFront distribution by replicating the required custom headers.
Lambda@Edge maybe?
To maintain the integrity of my CloudFront distribution and prevent circumvention, using Lambda@Edge with CloudFront seems to be a robust and viable approach. Lambda@Edge allows me to implement custom logic and URL rewriting directly at the edge locations, ensuring that requests go through my CloudFront distribution and are processed according to my desired rules. This provides greater control and security compared to relying solely on S3’s static website hosting.
Using a Lambda@Edge function, I can intercept requests at the CloudFront edge locations and rewrite the URLs to the appropriate S3 object. This enables me to perform the necessary URL rewriting to match the correct file in S3 for pretty URLs. This solution may seem like the ideal choice to enable pretty URLs without compromising existing security measures.
However, it’s important to consider the cost implications associated with Lambda@Edge. Lambda@Edge pricing is based on the number of requests and the duration of the function execution. The cost of $0.60 per 1 million requests or $0.0000006 per request is just too high for a humble blog or a low-traffic website, on top of that, I also have to factor in the cost per execution duration and amount of data processed - The price-point is too steep and complex for my needs.
Is there a solution similar to Lambda@Edge but cheaper? Enter CloudFront Function.
CloudFront Function
I’ve recently come across CloudFront Function as a possible alternative to Lambda@Edge, here are some reasons why I think CloudFront Functions could be the solution that can help me enable Pretty URLs at a cheaper price point:
Simplified deployment and management: CloudFront Functions are designed to be deployed directly within the CloudFront console, providing a simplified deployment and management experience. I can associate CloudFront Functions with my CloudFront distributions without the need for separate configuration or infrastructure management.
Ease of use and quick iteration: CloudFront Functions have a streamlined development and testing process. I can make changes to the function code and deploy updates faster compared to Lambda@Edge functions, as I don’t need to go through the separate Lambda function deployment process.
Reduced operational overhead: CloudFront Functions eliminate the need to manage and scale separate Lambda functions. With CloudFront Functions, I don’t have to worry about provisioning and scaling the underlying compute resources as it is handled by CloudFront.
Focused use cases: CloudFront Functions are designed specifically for request/response manipulation and processing at the edge locations. They are well-suited for simple use cases where there’s a need to modify headers, perform basic transformations, or make decisions based on request attributes.
Cost efficiency: CloudFront Functions have a pricing model based on the number of function invocations, invocation pricing is $0.10 per 1 million invocations ($0.0000001 per invocation), which can be more cost-effective for certain use cases compared to Lambda@Edge functions.
Secure: Since CloudFront Functions like Lambda@Edge sit on the edge so there’s no need to publicly expose the origin S3 bucket.
Given the reasons outlined above I have chosen to use CloudFront Function as a cost-effective, simple, and secure solution to implement Pretty URLs seamlessly without breaking old URLs.

Implementation
Writing a CloudFront Function
The first step is to write a CloudFront Function that appends index.html to viewer/user requests that do not include a file name or extension in the URL. This function intercepts the request and modifies it before passing it to the origin S3 bucket. If a user browses to /posts/2021/07/hello-world
or /posts/2021/07/hello-world/
, the CloudFront Function will append /index.html
or index.html
to the request, respectively.
In my case, since I am using Terraform to manage my infrastructure, I created a js/hugo-rewrite-pretty-urls.js
file inside my Terraform codebase.
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;
}
This CloudFront Function is particularly useful for single-page applications or statically generated websites hosted in an Amazon S3 bucket, which is perfect for my Hugo blog setup.
However, there is an important consideration to keep in mind. Implementing this function as-is will break old URLs. To ensure a smooth migration, I need to redirect old URLs to the new and prettier URLs. To handle this, I modify the function as follows:
function handler(event) {
var request = event.request;
var uri = request.uri;
// Permanently redirect any URLs ending with .html except for index.html
if (uri.endsWith('.html') && !uri.endsWith("index.html")) {
var response = {
statusCode: 301,
statusDescription: 'Permanently moved',
headers: {
"location": { "value": request.uri.replace(/\.[^/.]+$/, "/") }
}
}
return response;
}
// 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;
}
With this modification, when a viewer/user accesses a URL like /posts/2021/07/hello-world.html
, they will be redirected to the prettified URL /posts/2021/07/hello-world/
. However, if they access /posts/2021/07/hello-world/index.html
, no redirect will occur, and the request will proceed as usual.
Deploying the CloudFront Function by updating Terraform
Great! Now I have a function that will allow me to implement pretty URLs seamlessly without breaking old URLs! The next step is to deploy the CloudFront Function that I just created. I already have existing terraform code that deploys my blog infrastructure so I just need to modify that by editing my main.tf
and adding a resource block to create and deploy a CloudFront Function.
resource "aws_cloudfront_function" "this" {
name = "hugo-rewrite-pretty-urls"
runtime = "cloudfront-js-1.0"
comment = "Enable pretty URLs for Hugo"
publish = true
code = file("${path.module}/js/hugo-rewrite-pretty-urls.js")
}
This code block creates a CloudFront Function named “hugo-rewrite-pretty-urls” using the JavaScript runtime. The code attribute points to the js/hugo-rewrite-pretty-urls.js
file I created earlier.
Next, is to associate the new CloudFront Function to my existing CloudFront distribution. Inside the aws_cloudfront_distribution.this
resource declaration, add a function_association
block to the default_cache_behavior
block. I then make sure to set the event_type to “viewer-request” so that the function is invoked when the viewer/user sends a request. Here’s an example of how to add the function_association
block:
resource "aws_cloudfront_distribution" "this" {
enabled = true
price_class = "PriceClass_All"
aliases = [format("%s", var.blog_config.domain_name)]
...
default_cache_behavior {
target_origin_id = "myS3Origin"
....
function_association {
event_type = "viewer-request"
function_arn = aws_cloudfront_function.this.arn
}
...
}
...
}
Once these modifications are made, run terraform plan
and terraform apply
to create and deploy the CloudFront Function and associate it with my existing CloudFront distribution.


Reconfiguring, rebuilding, and redeploying my blog
Sweet! now that I’ve updated my blog’s infrastructure to support Pretty URLs the next step is to reconfigure, rebuild, and redeploy my Hugo-powered blog to actually implement Pretty URLs, luckily doing this with Hugo is very easy! To reconfigure my blog to use Pretty URLs I just needed to remove/delete any occurrence of uglyURLs = true
from my config.toml
file. Then I submitted a pull request and merged my changes, this will trigger a rebuild and redeploy my blog.

Now that I’ve enabled pretty URLs anyone who browses to https://jrpospos.blog/posts/2021/08/building-my-blog-part-1.html will be redirected to https://jrpospos.blog/posts/2021/08/building-my-blog-part-1/ and all URLs for any new content/post will be user-friendly and SEO-friendly.