Hosting Custom Fonts in AWS

11/27/2023185 views • 11 min read

Alright, here's the scenario: You're a front-end developer who was just given a stunning custom font from your designers, and they're incredibly excited about it. They want to use it as soon as possible. However, you're only familiar with using fonts from Google Fonts and are unsure how to handle these ttf/woff2 files. You find yourself asking questions like, "Where should I place these files?" and "How do I write the appropriate CSS to utilize them?". Fear not; I recently faced a similar situation and would like to share with you exactly how I handled this situation.

In this post, we will guide you step by step on how to take custom font files and host them on a Content Delivery Network (CDN) in AWS. We'll be using the AWS CDK, TypeScript, and Bun to manage various AWS resources, including S3 and CloudFront, to deliver your stunning custom fonts to the edge, close to your users and apps.

We will then build a new Next.js app in a Bun monorepo. This Next.js app will use our new CDN to pull in our new fonts. We'll write the CSS and font-face code to properly use our new font.

Project Setup

To start we need to set up our new project. Were going to be using Bun, TypeScript, and the AWS CDK. Utilizing these tools together allows us to use a single language, TypeScript, across our entire repository. It will also make it incredibly easy to transition to a monorepo powered by Bun later on in this article.

cdk init

Begin by opening your terminal and running mkdir how-to-host-custom-fonts-in-s3 to create a new directory specifically for this project.

Next, navigate to this new directory with cd how-to-host-custom-fonts-in-s3, and run bunx cdk init app --language typescript. Bunx is an alias for `bun x` and functions similarly to `npx`.

mkdir how-to-host-custom-fonts-in-s3
cd how-to-host-custom-fonts-in-s3
bunx cdk init app --language typescript

nvm use

Next, we'll need to run a couple more commands to properly set up Node and install our dependencies.

I usually have multiple work and personal-related projects on my laptop, so managing Node.js versions across all of them is crucial. I prefer using a .nvmrc file at the root of my projects. For this project, we will utilize Node.js version 20.

Let's create a .nvmrc file at the root of our project and set its value to v20, which is the latest version of Node.js.

VSCode with a .nvmrc file created with value v20

Great! Now just a few more commands to finish setting up our development environment.

nvm install v20
nvm use
  1. nvm install v20: Installs Node.js version 20 using Node Version Manager (nvm).

  2. nvm use: Activates the installed Node.js version for the session.

bun install

Now we are going to switch from npm to Bun as our package manager. I'm personally a big fan of Bun and have been slowly migrating all of my existing projects to use it. For any new projects, Bun is a default for me.

The AWS CDK scaffolds new projects using NPM as the package manager. To switch to Bun, we need to delete the package-lock.json and run bun install. Simply run the following commands to do so.

rm package-lock.json
bun install
  1. rm package-lock.json: Removes the package-lock.json file that was created by our cdk init command earlier.

  2. bun install: Installs project dependencies using Bun.

If everything goes smoothly, your folder structure should resemble the image below. The most crucial aspect is that you now have a bun.lock file. This lockfile functions differently from npm or yarn, as it is a binary file rather than plaintext. The primary reason for this is to enhance performance. Bun’s lockfile saves & loads quickly and saves a lot more data than what is typically inside lockfiles.

Image of VSCode after running the cdk init command and re-installing packages using bun

.prettierrc.json

The following configuration is based on personal preference. Since I have a widescreen monitor, I prefer to set my Prettier printWidth wider than the default. In this case, I'm using a value of 120.

VSCode with a .prettierrc.json file created with printWidth of 120

Building the stack

For our font hosting and delivery setup, we will be implementing several key AWS services:

  • S3 Bucket: Used for reliable and organized storage of font files. It's the main storage component.

  • CloudFront Distribution: Acts as the delivery mechanism, ensuring rapid distribution of fonts to users globally.

  • CloudFront Origin Access Identity (OAI): A security feature that allows CloudFront to securely access S3 fonts without public access.

  • CloudFront Cache Policy: Dictates caching behavior to optimize performance and costs, affecting load times and bandwidth.

  • CORS Configuration: Essential for cross-origin resource sharing, it enables secure usage of fonts across different domains.

CDK stack code with an S3 bucket, Cloudfront Distribution, Origin Access Identity, and Cache policy

Let's break down exactly what is happening here, line by line.

  • Imports: Bringing in necessary CDK modules for S3, CloudFront, and CloudFront origins.

  • Stack Definition: Creating a custom stack class HowToHostCustomFontsInS3Stack extending the CDK Stack.

  • S3 Bucket Setup: Initializing an S3 bucket with a removal policy and CORS configuration, allowing GET requests from any origin.

  • Origin Access Identity: Creating a CloudFront Origin Access Identity (OAI) for secure access to the S3 bucket contents.

  • Bucket Permissions: Granting read permissions to the OAI for the S3 bucket.

  • Cache Policy: Defining a CloudFront cache policy to control header behavior, particularly for the 'Origin' header.

  • CloudFront Distribution: Setting up a CloudFront distribution with the cache policy, an S3 origin linked to the OAI, and enforcing HTTPS.

  • CDK Output: Outputting the domain name of the CloudFront distribution for easy access post-deployment.

It's time to deploy this stack to AWS. Simply run bun cdk deploy --require-approval never. If all goes well, you will see an output similar to the one below. Pay particular attention to the distribution domain name output, as we will use it later to construct the URL for fetching the font files.

Image of terminal output after running bun cdk deploy

That's it! In just about 45 lines of TypeScript and AWS CDK code, you can create a CDN using CloudFront, backed by S3, to host your custom fonts. These fonts will be readily available at the edge, wherever your users access your web application.

Upload Fonts to S3

For simplicity, we will use the Montserrat font from Google, but the specific font is not important. If you have a completely custom font, this same solution will work just as well! This also works for any type of font file, including TTF, OTF, and WOFF.

  1. Log in to AWS Management Console: Open your AWS account and navigate to the S3 service.

  2. Select Your S3 Bucket: Locate and access the S3 bucket designated for the 'Montserrat' font files.

  3. Upload 'Montserrat' Folder: Click on 'Upload', then 'Add folder', and select the 'Montserrat/static' folder which includes various font files like 'Montserrat-Regular.ttf'. Ensure the MIME type for each file is set to 'font/truetype'.

    Image of Mac finder about to upload our font files

  4. Review and Upload: Double-check that the MIME types are correct and proceed with the upload. If the MIME type is incorrect, you may encounter a CORS error later, which can be a nightmare to debug.

    Image of AWS S3 with all of our font files ready to be uploaded

  5. Verify Upload: Refresh the bucket post-upload to confirm the Montserrat folder and its contents are uploaded. To test the 'Montserrat-Regular.ttf', use this URL format in your browser: https://dkeeepdefzien.cloudfront.net/Montserrat/static/Montserrat-Regular.ttf. This URL combines your CloudFront domain with the file path, allowing you to verify the successful hosting of the font file.

  6. Image of a browser with the address highlighted showing the full url to one of our font files

    Test with cURL: The last thing to verify is to make sure the proper Content-Type header is set. This is important for browsers to handle the font file correctly.

curl -I https://dkeeepdefzien.cloudfront.net/Montserrat/static/Montserrat-Regular.ttf

If everything is working, you should be able to download the file by navigating directly to https://dkeeepdefzien.cloudfront.net/Montserrat/static/Montserrat-Regular.ttf. Also, the cURL command should produce output like the following.

Image of a terminal output from a curl command targeting our CDN

Congratulations! You now have a functioning CDN hosting your custom font, which you can download directly from your web app.

Extra Credit

Now let's have some fun! In this section, we are going to ...

  1. Convert our project to a Bun monorepo

  2. Add a Next.js app

  3. Fetch and use our custom fonts

Convert to a Monorepo

Monorepos with Bun are straightforward and have the added benefit of hoisting common dependencies to the root of the repository.

  1. From the root, create the folder /packages/fonts-cdn

  2. Move everything from the root to /packages/fonts-cdn

  3. Delete /packages/fonts-cdn/node_modules

  4. Update the name of /packages/fonts-cdn/package.json to be "fonts-cdn"

  5. Create package.json in the root and set it to the following

    {
      "name": "how-to-host-custom-fonts-in-s3",
      "version": "1.0.0",
      "workspaces": [
        "packages/*"
      ]
    }
    
  6. From the root, run bun install

Your project should now resemble the structure below:

Image of VSCode after migrating to a bun monorepo

Create a Next.js app

Now let's scaffold a Next.js using create-next-app and Bun.

cd packages
bun create-next-app

Step through the normal create-next-app prompts and you should see an output that looks like this.

Image of output from running bun create-next-app

After completing the create-next-app prompts, your project should resemble the following structure. You will notice that there are now two packages. The first one is "fonts-cdn", which was our AWS CDK app from part 1 of this walkthrough. The second one is our newly created Next.js app called "fonts-app".

Great! Let's run the Next.js app in development mode and ensure everything works properly. From what I understand, with Bun, you need to change directories (cd) into the package you want to run and then execute the development command. This is slightly different from Yarn, where you can stay at the root and run the 'yarn workspace' command followed by the desired command.

cd packages/fonts-app
bun dev
open http://localhost:3000

You should see the iconic Next.js default page!

Image of the default Next.js page layout after running bun dev

Use the Font CDN

Fantastic, we now have all the components necessary to utilize our custom fonts hosted on our CDN. When it comes to using a custom font with Next.js and Tailwind, there are several options for customizing Tailwind.

I recommend these docs from Tailwind Toolbox, which explain the differences between various Tailwind font customizations. Essentially, you can either overwrite or extend the default Tailwind theme, and your approach will affect the appearance of your generated Tailwind utility classes.

For the sake of brevity, let's host our CSS locally and overwrite the existing Tailwind utility classes. However, this decision ultimately depends on your specific use cases.

Create montserrat.css

First, we must create a CSS file that defines our font faces. The crucial aspect is to establish a font face for each font file, taking into account font weight and italics. Call this CSS file 'montserrat.css' and place it in fonts-app/src/app/montserrat.css.

Image of motserrat.css with all our custom font faces

Update globals.css

Next, make a one-line change to 'fonts-app/src/app/globals.css' to include 'montserrat.css'. Simply use the @import statement in 'globals.css' to incorporate the new CSS file. After making this change, your 'globals.css' should resemble the following.

Default Next.js globals.css with the mtserrat.css file included

Delete next/font/google

The latest version of Next.js comes with an excellent font optimization system. However, it doesn't directly align with our use case since we want to host our font on a CDN for multiple apps to utilize. Therefore, we will remove the font optimization code for now.

Navigate to fonts-app/src/app/layout.tsx and eliminate any usage of next/font/google and Inter. Once completed, your layout.tsx should resemble the following.

Default Next.js layout.tsx with any font optimizations removed

Update tailwind.config.ts

Since we are using Tailwind, we need to inform it that we want to use Montserrat as our sans-serif font family. Tailwind makes this process quite straightforward. There are several ways to customize your font family with Tailwind, depending on your use case and objectives. For this walkthrough, we will override the default sans-serif font so that the out-of-the-box utility classes seamlessly use our Montserrat font. To accomplish this, update your tailwind.config.ts file, specifically the config.theme.fontFamily.sans value. Once updated, it should look like the following.

Image of tailwind.config.ts after adding the Montserrat as the default fontFamily sans font.

Notice that we are setting the sans font family to an array, with the primary value as Montserrat and using the spread operator to include the default settings as backups.

Testing it all out

Awesome! Now let's test our new fonts and setup. Restart your dev server by running the following commands.

cd packages/fonts-app
bun run dev
open http://localhost:3000

If everything is working, the default Next.js page should now be using the Montserrat font and should look like the following image.

Image of the browser with dev tools open showing that the default Next.js font is now Montserrat

Thanks for reading!

Congratulations! If you made it this far, you successfully hosted custom font files on a Content Delivery Network (CDN) in AWS using AWS CDK, TypeScript, and Bun. We covered setting up the project, building and deploying the stack, uploading fonts to S3, and utilizing the CDN with a Next.js app.

If you found this content helpful, please consider liking, sharing, and following me here on Hashnode and at https://twitter.com/thealexkates.