Personalize your static site based on a previous site referral
Tags: javascript, webdev, serverless
Published: 2022-10-26 10:30:27

My blog posts are often shared on Reddit, Hacker News and other similar sites. Hooray for more post views! But the internet is often a *wild* place, and some people forget that us writers are real human beings with thoughts and feelings and tricky brain chemistry. And so I had an idea for an experiment. What if I could show people a friendly message on my website based on the site they came from, to remind readers that I’m real?

There were two conditions that influenced my solution. 

- My blog site is a static site — currently built using [Eleventy](https://www.11ty.dev/)
- I didn’t want to use client-side JavaScript — I’m currently on a mission [to use as little client-side JS as possible](https://whitep4nth3r.com/blog/how-i-improved-website-performance/)

Enter Netlify Edge Functions, which allow us to intercept and modify HTTP requests and responses based on cookies, geolocation data,  request headers and any custom logic you might need. You can add this functionality to any new or existing project on Netlify — even if your site is pre-generated at build time and served as static files from Netlify’s CDN (like mine is), all with no client-side JS. 

In addition to showing messages based on where my blog visitors came from, this can also extend to many similar real-world use-cases, including 
- showing targeted discount codes based on the site you came from
- prioritising news or product content based on the previous site you visited

Were you browsing the Apple site for a new iPhone? Visit your network provider’s home page next and they can show you their iPhone plans first!

Let’s take a look at how we can do this with Edge Functions. 

## Using the HTTP referer header

With Netlify Edge Functions, we can access the values of HTTP request headers, and act based on the value of those headers. We can rewrite URLs, modify HTML, set cookies, rewrite those headers, and more — all at the server-level without using client-side JS.

The [HTTP referer header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer) contains an absolute or partial address of the page that makes the request, allowing a server to identify where visitors came from — i.e. where they have been **referred** from — as long as the [noreferrer attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/noreferrer) is **not** set on the anchor tag of the inbound link.

## The tutorial

In this tutorial, you’ll learn how to detect the HTTP referer header and enhance a static HTML page in five steps:

1. Add a static HTML page to a fresh project
2. Create a new Edge Function file and configure it to run on the static HTML page
3. Add a placeholder to the HTML page to modify using the Edge Function
4. Grab the HTTP referer header from the request
5. Use HTMLRewriter to modify the placeholder in the HTML response if the referer header matches a particular string

### Why not do this with client-side JavaScript?

As always, there’s more than one way to make the magic happen. You _can_ achieve similar results using client-side JavaScript, but using an Edge Function is better for resilience, performance, and user experience.

#### Resilience

Edge Functions intercept the HTML response, allowing the HTML to be transformed it before it’s sent to the browser. This is more robust; if users have JavaScript disabled, they’ll still see the transformed HTML. And this all works on static HTML/pre-generated pages.

#### Performance and user experience

JavaScript in the browser takes time to load, run, detect the HTTP referer header, and modify the DOM. As well as adding precious extra milliseconds to page load times, with this method users will likely experience Cumulative Layout Shift (CLS) — where the layout of the page shifts some time after the initial page content loads and new content is rendered. As well as not being ideal for the user, [Google may also penalise your site in search engine results](https://web.dev/cls/).

Let's get coding! 👩🏻‍💻✨

### Add a static HTML page to a fresh project

Create a new project directory, and add an `index.html` file. Open the file in your IDE of choice, and add some HTML boilerplate. Here’s some boilerplate I prepared earlier.

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>HTTP referer header tutorial</title>
  </head>
  <body>
    <h1>HTTP referer header tutorial</h1>
  </body>
</html>
```

### Add a new Edge Function to your project

At the root of your project:

- create a `netlify` directory
- inside that, add an `edge-functions` directory
- inside that, add a new file, `referer.js`

This is where we’ll write the code for the Edge Function. Here’s what your project structure should look like so far.

```text
# project structure

├── index.html
└── netlify
    └── edge-functions
        └── referer.js
```

Next, add a `netlify.toml` file to the root of your project. This is a configuration file that specifies how Netlify builds and deploys your site — including redirects, branch settings, Edge Function declarations and more. In your `netlify.toml` file, add the following code. 

```toml
# netlify.toml

[[edge_functions]]
  path = "/"
  function = "referer"
```

This tells Netlify to run the `referer` Edge Function when a visitor lands on `index.html`. In a real-world example, you may want to configure the Edge Function to run on a group of specific paths, or all paths. Learn more about [Edge Functions declarations](https://docs.netlify.com/edge-functions/declarations/) on the Netlify docs.

_**Note:** This tutorial is a framework-agnostic method which shows Edge Functions working out of the `netlify` directory. If you’re using a framework, you can still follow this tutorial successfully, but you may want to take advantage of the workflow and feature benefits Netlify provides for other specific technologies._

_For example, if you’re an avid user of Next.js, you can get to work straight away in your `middleware.ts` file, where you have full access to the HTTP request and response on Netlify, allowing you to [modify HTML and update page props](https://www.netlify.com/blog/rewrite-html-transform-page-props-in-nextjs/) on both static and dynamic pages. This is automatically supported in all Next.js sites on Netlify, powered by Netlify’s Next.js Runtime and Advanced Middleware capabilities. It just works™️._

### Add a placeholder to modify in your HTML file

Next, update your `index.html` file and add an empty placeholder element. Add `id=referer` to the element, or a similar selector of your choosing. This is how we’ll target the HTML response for rewriting in the Edge Function.

```diff-html
  <!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <title>HTTP referer header tutorial</title>
    </head>
    <body>
+      <p id="referer"></p>
       <h1>HTTP referer header tutorial</h1>
    </body>
  </html>
```

### The Edge Function code

Let’s set up the Edge Function to intercept requests. In `referer.js`, add the following code. Netlify Edge Functions take two parameters: `request`, which represents the incoming HTTP request, and `context`, which is a [Netlify specific API](https://docs.netlify.com/netlify-labs/experimental-features/edge-functions/api/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=ef-context-object#netlify-specific-context-object) that exposes geolocation data, lets you work with cookies, rewrites, site information and more.

```javascript
// netlify/edge-functions/referer.js

export default async (request, context) => {

};
```

Let’s get the value of the referer header from the request. Given Netlify Edge Functions use standard web APIs, we can do this using the [Headers.get() method](https://developer.mozilla.org/en-US/docs/Web/API/Headers/get).

```diff-javascript
  // netlify/edge-functions/referer.js

  export default async (request, context) => {
+   // get HTTP referer header value
+   const referer = request.headers.get("referer");
  };
```

Let’s do some defensive coding and return early if we don’t find a referer header (for example if the `noreferrer` attribute is set on the inbound link). Add the following code to get the next HTTP response in the chain by awaiting `context.next()`, and return it if `referer === null`. This tells the Edge Function to serve the `index.html` file that had been requested without us making any changes to it along the way.

```diff-javascript
  // netlify/edge-functions/referer.js

  export default async (request, context) => {
    // get HTTP referer header value
    const referer = request.headers.get("referer");

+   // get the next HTTP response in the chain
+   const response = await context.next();

+   // if no referer, return the response
+   if (referer === null) {
+     return response;
+   }

  };
```

Next, when you’ve decided on the referer header value that you want to target, set up a check to return the response early if the incoming referer doesn’t match. In this example I’m using the Reddit referer `https://www.reddit.com/`. Note that this is a basic equality check example and you may want to get a little more fancy with fuzzy checks or some logic based on multiple values.

```diff-javascript
  // netlify/edge-functions/referer.js

  export default async (request, context) => {
    // get HTTP referer header value
    const referer = request.headers.get("referer");

    // get the next HTTP response in the chain
    const response = await context.next();

    // if no referer, return the response
    if (referer === null) {
      return response;
    }

+   // check for the referer value you care about
+   // return if it doesn't match
+   if (referer !== "https://www.reddit.com/") {
+     return response;
+   }

  };
```

Next, it’s time to modify the HTML response when we find a referer value match. There are a number of ways we can modify the HTML, including by parsing the HTML as a string and finding and replacing using a regular expression. For this example, however, we’re going to import and use [Cloudflare’s HTMLRewriter](https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/) because it offers us the ability to target and modify the HTML response with the kind of JavaScript syntax we’re familiar with using in the browser. 

Netlify Edge Functions are powered by the Deno runtime, and package imports are supported via URL imports rather than local package installation, as in the case of Node.js environments. Add the URL import to HTMLRewriter at the top of the file.

```diff-javascript
  // netlify/edge-functions/referer.js
+ import { HTMLRewriter } from "https://ghuc.cc/worker-tools/html-rewriter/index.ts";

  export default async (request, context) => {
   // ...
  }
```

Finally, add the following code. This updates the HTML in the HTTP response by finding the element we want to modify (the paragraph tag with `id="referer"`), and setting its content to “Hello, Reddit user!”

```diff-javascript
  // netlify/edge-functions/referer.js
  import { HTMLRewriter } from "https://ghuc.cc/worker-tools/html-rewriter/index.ts";

  export default async (request, context) => {
    // ...

+   // if we do have a referer match, rewrite the element
+   // in the response HTML with a friendly message
+   return new HTMLRewriter()
+     .on("p#referer", {
+       element(element) {
+         element.setInnerContent("Hello, Reddit user!");
+       },
+     })
+     .transform(response);
  };
```

The above is just a text example. If you want to add HTML to your referrer message, perhaps to include an onward link, set `html: true` on `setInnerContent`, like so.

```javascript
element.setInnerContent(
  `Hello, Reddit user! <a href="https://go">Visit this link!</a>`, 
  {html: true}
);
```

You may also want to hide or show the paragraph tag based on whether it’s rewritten or not. You might need to do this if your referrer message is styled with a background color and padding which you don’t want to show when the element is empty. In this case, you can hide the element by default using CSS, and use HTMLRewriter to add a class to the element during the HTML transformation.

```diff-javascript
  // netlify/edge-functions/referer.js
  import { HTMLRewriter } from "https://ghuc.cc/worker-tools/html-rewriter/index.ts";

  export default async (request, context) => {
    // ...

    return new HTMLRewriter()
      .on("p#referer", {
        element(element) {
          element.setInnerContent("Hello, Reddit user!");
+         element.setAttribute("class", "showMessage");
        },
      })
      .transform(response);
  };
```

Alright, now we’re ready to test this out in development!

## Testing in local development

We can use the Netlify CLI to spin up a development server and run the Edge Function code. Install the Netlify CLI globally on your machine by running the following command in your terminal.

```text
npm install netlify-cli -g
```

**Before we start the development server, a note about HTTP headers.** Unfortunately there’s no way to reliably fake or force values on HTTP headers for security reasons. To test that the HTML is rewritten in a development environment, we need to make two changes. 

First, change the referer header value check to your development environment URL in `referer.js`.

```diff-javascript
  // netlify/edge-functions/referer.js

  export default async (request, context) => {
    //...

-   if (referer !== "https://www.reddit.com/") {
+   if (referer !== "http://localhost:8888/") {
     return response;
    }

    //...
  }
```

And on `index.html`, add an anchor tag that points to the same page — `/`. Clicking on this link will set the HTTP referer header to the development URL.

```html
// index.html

<a href="/">Click here to set the referer header</a>
```

Start the development server by running the following command in your terminal.

```bash
netlify dev
```

Navigate to [http://localhost:8888/](http://localhost:8888/). **You won’t see the rewritten HTML yet! There's no previous site, so there's no HTTP referer header set.** Click on the link you just created to set a referer header. And when the page reloads, you’ll see that the HTML has been transformed by the Edge Function! 🎉

To remove the HTTP referer header in development, navigate to [http://localhost:8888/](http://localhost:8888/) again **without clicking on the link on the page**. Open up the network tab to watch the HTTP referer header appear and disappear depending on your behaviour!

### Bonus content: contextual environment variables

To avoid having to change the HTTP referer header to `localhost` in development, you may wish to use an [environment variable](https://docs.netlify.com/environment-variables/overview/) when you deploy your site. To do this, set an `HTTP_REFERER_CHECK` environment variable in your production site, such as `https://www.reddit.com/` using the Netlify UI or the [CLI](https://cli.netlify.com/commands/env). Then, override the value in your development environment by adding the following entry to the `netlify.toml` file.

```toml
# netlify.toml

[context.dev.environment]
  HTTP_REFERER_CHECK = "http://localhost:8888/"
```

Finally, in `referer.js`, replace the string equality check with the environment variable, retrieved using the `Deno env API`:

```diff-javascript
  // netlify/edge-functions/referer.js

  export default async (request, context) => {
    //...

-   if (referer !== "http://localhost:8888/") {
+   if (referer !== Deno.env.get("HTTP_REFERER_CHECK")) {
     return response;
    }

    //...
  }
```

And lastly, you may want to check for multiple referer header values — in which case a single string may not suffice, and you may want to do something like I do on [my website](https://github.com/whitep4nth3r/mk2-p4nth3rblog/tree/main/src/_edge-functions/referer.js).

If you’re ready to deploy, [add git version control and deploy using the Netlify CLI by following this guide](https://www.netlify.com/blog/add-personalization-to-static-html-with-edge-functions-no-browser-javascript/#add-git-version-control).

## Let’s recap

In this tutorial, we created a new project that served a static HTML file. We used an Edge Function to detect the value of the HTTP referer header, and transform the HTML response to add a sprinkle of ✨personalization✨. We also looked at how we could replicate functionality seamlessly in development and production environments using environment variables.

To see this code in action, [visit the live demo site](https://personalize-with-http-referer.netlify.app/) — which checks to see if you’ve been referred from Netlify, and includes the same functionality we set up in this guide. And finally, check out the [demo repository on GitHub](https://github.com/whitep4nth3r/personalize-with-http-referer) to explore the code, fork it, and make it your own.

![Screenshot of demo website, showing a personalized message at the top, including the message you can from the netlify blog post, showing it personalized the page based on the netlify.com referer header](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/09og521ao3ujw0ozi4yr.png)

## Next steps

You’ve learned how you can modify static HTML pages based on the HTTP referer header, but did you know you can also perform actions based on other things like location, time, cookies and more? Check out the official [Netlify Edge Functions docs](https://ntl.fyi/3Qojhck), or browse our [Edge Functions examples site](https://edge-functions-examples.netlify.app/) for some inspiration. Happy coding! 👩🏻‍💻