Pretty URLs with CloudFront and S3

Posted on September 24, 2023 • 3 min read

If you have ever set up static site hosting using S3 Bucket, your website URL probably looks somewhat like the following: example.com/index.html

There is nothing wrong with the URL except one tiny problem. It does not look beautiful at all. Ideally, the site should be reachable at example.com. But now, the user has to append index.html to the end of the URL to see the webpage.

The easy solution is to set CloudFront DefaultRootObject to index.html. So whenever the user requests example.com, CloudFront returns index.html at the root of your S3 Bucket.

├── blog
│   ├── my-first-blog
│   │   ├── index.html
│   ├── my-second-blog
│   │   ├── index.html
├── index.html

Given the folder structure above, you can reach example.com just fine. However, you cannot reach example.com/blog/my-first-blog at all. This is because S3 Bucket interally is a key value store, and there is no object with the name blog/my-first-blog in S3 Bucket.

To solve the problem above, we need to make use of CloudFront Function

Using CloudFront Functions, we can implement default directory indexes for all bucket directories. At the time of writing, there are some limitations with CloudFront Functions, notably that it only supports JavaScript ES 5.1.

Make sure that your function is triggered on viewer request event.

The following code does the trick:

/**
 * The function should be triggered on CloudFront Viewer Request event
 * handler is the entry point
 * Assume that if the last segment of the URL contains a .
 * (dot), it is requesting a file
 */
function handler(event) {
    var response = {
        statusCode: 301,
        statusDescription: "Moved Permanently",
        headers: {},
        cookies: {},
    };
    var host = event.request.headers.host.value;

    // if the requested resource is not a file, 
    // redirect to trailing slash
    if (!haveTrailingSlash(event.request.uri) &&
     !haveFilePostfix(event.request.uri)) {
        response.headers = {
            'location': {
                "value": `https://${host}${event.request.uri}/` 
            }
        }
        return response;
    }

    // Redirect index.html to / to avoid duplicate contents
    if (haveIndexHtmlPostfix(event.request.uri)) {
        var updatedUri = event.request.uri.slice(0, -"index.html".length)
        response.headers = {
            'location': {
                "value": `https://${host}${updatedUri}`
            }
        }
        return response;
    }

    if (haveTrailingSlash(event.request.uri))
        event.request.uri += "index.html";

    return event.request;
}

function haveFilePostfix(requestUri) {
    var lastSegment = requestUri.split("/").pop();
    if(!lastSegment)
        return false;
    return lastSegment.includes(".");
}

function haveTrailingSlash(requestUri) {
    return requestUri.endsWith("/");
}

function haveIndexHtmlPostfix(requestUri) {
    var lastSegment = requestUri.split("/").pop();
    return lastSegment === "index.html" ? true : false;
}

To summarize, there are 3 use cases:

The code allows:

Limitations: