How to add hotlink protection to your web fonts with Netlify Edge Functions and Deno
Tags: webdev, javascript, tutorial, netlify
Published: 2023-05-05 01:33:22

### Because the people crafting quality typefaces deserve to be paid

While working on my redesigned personal website [sidney.me](https://sidney.me/?utm_source=dev.to&utm_medium=Blog&utm_campaign=How+to+add+hotlink+protection+to+your+web+fonts+with+Netlify+Edge+Functions+and%C2%A0Deno), I chose to use a paid font, [Klim Type Foundry](https://klim.co.nz/)’s *[The Future Mono](https://klim.co.nz/retail-fonts/the-future-mono/)*. It was my first time using a paid web font for a project, so I read (skimmed) the [web font licence agreement](https://klim.co.nz/licences/web-fonts/). Curiously, it included this clause (emphasis mine):

> **3d. Web Fonts File Protection**  
You agree to use reasonable measures to ensure the Web Fonts are available only for the process of styling text on Your Website. At a minimum, and by way of illustration not limitation, reasonable measures include a.) preventing unlicensed third-party access, i.e. **hotlinking** and b.) **not allowing direct download** of the Fonts unrelated to the process of styling text for Your Website.

In simpler terms, you must make sure you deploy the web font files so that no one can paste the URL in their browser to download it and that other sites can’t use the web font by linking to that URL.

I’ve been using Netlify for years to host my websites, so I searched for a way to do it natively. I came across this [Netlify support forum thread](https://answers.netlify.com/t/hotlink-protection-for-web-fonts/2025) that unfortunately concluded with, “We don’t really have a solution for this particular problem yet, but it’s one that might be covered in the future. Stay tuned!” In March 2020.

The replies did have some suggestions, like:

- using Netlify Functions (overkill for this use case and potentially slow) to check the [`Referer` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer) in the HTTP request,
- using a more performant Cloudflare Worker to [do that check](https://developers.cloudflare.com/workers/examples/hot-link-protection) (but I don’t want to sign up for another service), or
- serving the fonts from an S3 bucket or separate Apache server (breaking the whole point of the serverless stack) and using `.htaccess`.

Since the last reply on that thread, Netlify has released [Edge Functions](https://www.netlify.com/blog/announcing-serverless-compute-with-edge-functions/). They run JavaScript or TypeScript code using [Deno](https://deno.land/), the hot new JS runtime meant to succeed Node, on CDN servers that are much closer to users than where typical serverless functions run. In other words: fast, fast, fast.

## The code

Setting up Edge Functions on your Netlify project is incredibly simple. Start by creating a `netlify/edge-functions` directory and making a JS or TS file in that. Then, export a default function for Netlify to run. Let’s call it `hotlink-protection.ts` and return “Hello world” as a basic response:

```tsx
// netlify/edge-functions/hotlink-protection.ts

export default () => new Response("Hello world");
```

Then, we need to specify which routes this function should run on. In this case, we can limit it to font files. I only serve fonts in the WOFF2 format, as [browser support is excellent](https://docs.netlify.com/edge-functions/get-started/). In the root of your project, create a file called `netlify.toml`, and set the path and the function name to be the same as the file name you chose.

```toml
# netlify.toml

[[edge_functions]]
	path = "/*.woff2"
	function = "hotlink-protection"
```

While we’re here, let’s add a cache header for our fonts. They’re immutable, static assets that won’t change: we need to specify that to the browser.

```toml
# netlify.toml

[[headers]]
	for = "/*.woff2"
	[headers.values]
		Cache-Control = "public, max-age=31536000, immutable"
```

Now let’s get it working. We’re effectively creating middleware that modifies and returns the response—either the font file itself or a 403 Forbidden HTTP error. Edge Functions receive two arguments, `request` and `context`. We’ll need to `await` the response (the font data) using `context`, so let’s change it to an `async` function. Here’s the definition in TypeScript:

```tsx
import type { Context } from "https://edge.netlify.com";

export default async (
	request: Request,
	context: Context
): Promise<Response> => {
	// ...
};
```

Then, let’s get the `Referer` header from `request` and use a regular expression to test that it’s coming from our website. If there’s no `Referer` or it doesn’t match, return a 403.

```tsx
import type { Context } from "https://edge.netlify.com";

export default async (
	request: Request,
	context: Context
): Promise<Response> => {
	const referer = request.headers.get("referer");

	const regex = /^https?:\/\/(.*\.)?sidney\.me(\/.*)?$/;

	if (!referer || !regex.test(referer)) {
		return new Response("Forbidden", { status: 403 });
	}

	// ...
};
```

That regex looks gnarly, but most of its special characters are used to escape the special characters `/` and `.`, which are also part of URLs. It matches `sidney.me` and any subdomains and directories, so make sure to change those characters (excluding the `\.` in between) when copy-pasting this snippet. For an explanation of the regex, check it out on [regex101.com](https://regex101.com/r/Vs3fbH/1). The top-right panel highlights what each character does.

Then to return the font file as-is, we can use `await context.next()` to get the next HTTP response. Here’s the **final code**:

```tsx
// netlify/edge-functions/hotlink-protection.ts

import type { Context } from "https://edge.netlify.com";

export default async (
	request: Request,
	context: Context
): Promise<Response> => {
	const referer = request.headers.get("referer");

	const regex = /^https?:\/\/(.*\.)?sidney\.me(\/.*)?$/;

	if (!referer || !regex.test(referer)) {
		return new Response("Forbidden", { status: 403 });
	}

	const response = await context.next();
	return response;
};
```

Now you can commit your changes and deploy your project to Netlify as usual. Once deployed, navigating to the font URL should display “Forbidden”.

Note: if you want your fonts to work in **branch deploys**, you’ll need a separate check for your Netlify domain name. In my case, it’s `sidney-me.netlify.app` and the branch name and `--` is prefixed: `branch-name--sidney-me.netlify.app`. Here’s my regex:

```tsx
	const prodRegex = /^https?:\/\/(.*\.)?sidney\.me(\/.*)?$/;
	const devRegex = /^https?:\/\/(.*\--)?sidney-me\.netlify\.app(\/.*)?$/;

	if (
		!referer ||
		!(prodRegex.test(referer) || devRegex.test(referer))
	) {
		return new Response("Forbidden", { status: 403 });
	}
```

## Subset your font files for additional protection and a performance boost

Of course, this protection itself isn’t bulletproof. Someone can easily spoof the `Referer` header or use DevTools to download the font files. The font data must be sent to users for their browsers to display the font. Once it’s on the user’s machine, there will always be a way for them to extract the font file.

One way to mitigate this is to subset the font files only to include the characters you use on your site, excluding characters for other languages or writing systems. Also, fonts typically have [OpenType features](https://www.adobe.com/products/type/opentype.html) like [alternate characters](https://fonts.google.com/knowledge/introducing_type/introducing_alternate_glyphs), primarily for stylistic purposes, and we can follow [Google Fonts’ example](https://github.com/google/fonts/issues/1582#issuecomment-394168818) in removing them from the files we serve.

For my site, I used `pyftsubset` from FontTools to subset. Follow [these instructions](https://github.com/fonttools/fonttools#installation) to install (note that you’ll need Python installed too). Here’s an example:

```bash
pyftsubset the-future-mono-regular.woff2 \
--output-file="the-future-mono-regular--subset.woff2" \
--flavor=woff2 \
--unicodes="U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD"
```

This command subsets to the Latin character set and some punctuation symbols for a total of 385 characters. It also removes discretionary OpenType features as described in the [subset docs](https://fonttools.readthedocs.io/en/latest/subset/index.html#glyph-set-expansion). In my case, this more than halved the size of the font file, from 30.1 kB to 14.7 kB. This can add up and improve your Lighthouse performance scores.

Then specify the Unicode range in your CSS to help the browser use an appropriate fallback font if necessary.

```css
@font-face {
	font-family: "The Future Mono";
	src: url("the-future-mono-regular--subset.woff2") format("woff2");
	font-display: swap;
	font-weight: 400;
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
		U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
		U+FEFF, U+FFFD;
}
```

## Serve fonts from a different project

If you’re publishing your site’s code as open-source, make sure *not* to include your font files in that repo. You don’t want to be part of the problem of GitHub becoming the [web’s largest font piracy site](https://pixelambacht.nl/2017/github-font-piracy/).

1. Add your font files to your `.gitignore` file with `*.woff2`.
2. If you have already publicly published the repo, permanently remove the files from history ([instructions](https://pixelambacht.nl/2017/github-font-piracy/#fixing-the-problem)).
3. Then, deploy your font files in a new **private repository** as a separate Netlify project.
4. You can keep the files in your **local repository** for testing and leave your CSS unchanged, pointing to files in the same domain name. Then use a [rewrite](https://docs.netlify.com/routing/redirects/rewrites-proxies/) to point to the other project’s domain name like so:
    
    ```toml
    # netlify.toml
    
    [[redirects]]
      from = "/fonts/the-future-mono-*"
      to = "https://your-project.netlify.app/the-future-mono/the-future-mono-:splat"
      status = 200
      force = true
    ```
    

**Important:** you must have the hotlink protection Edge Function on both projects for the matching URLs. If someone goes to your primary URL to get the font, the Edge Function on the private project will get the correct `Referer` header and let anyone download or hotlink it.

You can copy the same function into your primary repository and set it to the appropriate paths in `netlify.toml`:

```toml
# netlify.toml

[[edge_functions]]
  path = "/fonts/the-future-mono-*"
  function = "hotlink-protection"
```

Now you have the best of both worlds: the font is available for local development while protected when deployed.

## Additional resources

- Here are some [suggestions from TypeKit](https://blog.typekit.com/2009/07/21/serving-and-protecting-fonts-on-the-web/) on additional protections for web fonts.
- Alternatively, you can use [glyphhanger](https://github.com/zachleat/glyphhanger) to subset web fonts based on what characters are used on your website.
- You can see which characters are in a Unicode range and do maths on ranges using [Unicode Range Interchange](https://www.zachleat.com/unicode-range-interchange/).
- You can see which OpenType features and all the characters are included in your font file using [Wakamai Fondue](https://wakamaifondue.com/).
- If you’re curious about web font performance, Zach Leatherman wrote an [excellent guide](https://www.zachleat.com/web/css-tricks-web-fonts/) on its implementation on CSS-Tricks.

---

And, of course, check out my personal site, [sidney.me](https://sidney.me/?utm_source=dev.to&utm_medium=Blog&utm_campaign=How+to+add+hotlink+protection+to+your+web+fonts+with+Netlify+Edge+Functions+and%C2%A0Deno), and let me know what you think on Mastodon, [@notsidney@indieweb.social](https://indieweb.social/@notsidney).


[![Hi, I’m Sidney.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rc85rty5kmorv51e740l.jpg)](https://sidney.me/?utm_source=dev.to&utm_medium=Blog&utm_campaign=How+to+add+hotlink+protection+to+your+web+fonts+with+Netlify+Edge+Functions+and%C2%A0Deno)