Middleware Redirects on Vercel

In the previous post we looked at how to set up a collection of static redirects via the vercel.json configuration file. Now we’re going to explore a more flexible and dynamic alternative using Edge Middleware.

In order to avoid confusion we’ll remove all redirects and rewrites that we implemented via vercel.json in the previous post.

🚀 TL;DR Show me the code. Look at the 22-vercel-redirect-middleware branch. This site is deployed here.

What Doesn’t Work

When I started investigating middleware the problem that I was trying to solve was not really “dynamic” redirects but rather redirects that were determined at build time.

To me the obvious way to set up redirects at build time is to write to the vercel.json configuration file. However, sadly, this doesn’t seem to work. If you create a redirect in this file and commit to the repository then the file remains intact at build time. If, however, you create or modify the vercel.json file during the build then it appears to either get clobbered or ignored.

However, middleware offers a convenient solution to this problem. In my recent experience it also solves a few other problems relating to redirects.

What is Edge Middleware?

Middleware (as the name implies) effectively lies between the client and the server. All requests pass through the middleware on their way to the server.

Edge Middleware in particular refers to server-side functions that run at the edge of the network, close to the user. These functions intercept and potentially modify HTTP requests before they reach the main server or static site. This enables developers to implement server-side logic such as redirects, rewrites, custom headers, authentication, A/B testing and bot protection. Executing these functions at the edge enhances performance by reducing latency, as computations are done closer to the user. They also provide scalability by using a global network of edge nodes.

An overview of the Vercel edge network can be found here.

Middleware

We will consider three approaches to implementing redirects via middleware:

  1. Manually specified redirects, where each redirect is implemented individually in code.
  2. Redirects from a file, a more flexible and scalable approach where a list of redirects is loaded from a file.
  3. Redirects from a file with placeholders, taking advantage of redundant redirect patterns.

Middleware functions are implemented in a middleware.js file located in the root directory of the project.

Manually Specified Redirects

The first approach would only be feasible in very limited circumstances where there are just a few redirects or each redirects requires very specific configuration.

export default async function middleware(request) {
    const url = new URL(request.url);

    if (url.pathname === "/what-is-asciidoc/") {
        return new Response(null, {
            status: 308,
            headers: {
                Location: '/1.2/what-is-asciidoc/',
            },
        });
    }

    return undefined;
}

This simple function will check if the path for the request is /what-is-asciidoc/ and, if so, respond with a 308 redirect to the path /1.2/what-is-asciidoc/. Otherwise the request will be handled normally (returning undefined allows the request to proceed unaltered to the server).

Redirects from File

Now let’s generalise this. Create a JSON file redirects.json that specifies the source and destination paths for each of the redirects. The file should be located in the project root directory.

[
    {
        "source": "/what-is-asciidoc/",
        "destination": "/1.2/what-is-asciidoc/"
    },
    {
        "source": "/what-is-gatsby/",
        "destination": "/1.2/what-is-gatsby/"
    },
    {
        "source": "/what-is-tailwind/",
        "destination": "/1.2/what-is-tailwind/"
    }
]

Now update middleware.js to read the contents of the file and create the required redirects.

import redirects from './redirects.json';

export default async function middleware(request) {
    const url = new URL(request.url);

    for (const redirect of redirects) {
        if (url.pathname === redirect.source) {
            return new Response(null, {
                status: 308,
                headers: {
                    Location: redirect.destination
                }
            });
        }
    }

    return undefined;
}

This approach allows you to handle a larger number of redirects in a more concise fashion (less code, more data).

Redirects from File with Placeholders

Looking at the JSON file above it should be apparent that there’s significant redundancy in the redirects and that they might be more economically specified via some sort of pattern. Let’s update the JSON file to use placeholders. We’ll just use a single placeholder, :topic, for the example below, but the final implementation will be able to handle multiple placeholders. I’ve also added three more rules which do not use placeholders.

[
    {
        "source": "/what-is-:topic/",
        "destination": "/1.2/what-is-:topic/"
    },
    {
        "source": "/blog/",
        "destination": "/1.2/"
    },
    {
        "source": "/latest/",
        "destination": "/"
    },
    {
        "source": "/ignore/",
        "destination": "/"
    }
]

Whenever the request path matches the pattern specified in source (treating the the placeholder :topic as a wildcard) the destination path is updated by replacing the placeholder with the text matched by the placeholder. For example, the request path /what-is-gatsby/ will fit the pattern with "gatsby" matching the placeholder, so that the corresponding destination path becomes "/1.2/what-is-gatsby/". With this in place the three redirects specified in the previous JSON file will still be valid, but similar redirects for the other three pages on the site will also now spontaneously work. 📌 Neither the source nor destination path needs to contain the placeholder, but if there’s a placeholder in destination then it needs to also appear in the corresponding source.

The required updates to middleware.js can be found here. 💡 The middleware placeholder matching implementation will match any combination of alphanumeric characters, underscores and periods.

🚨 Given the great flexibility available via this approach you might be tempted to try redirecting based on URL anchors. Unfortunately this is not possible via middleware (or by any mechanism on the server). But you can do it via dynamic client-side redirects.

Building Redirect List

If you want to construct or amend the list of redirects then you can do this after you have built the site by using the onPostBuild() API. This was really what I started out wanting to achieve. At the end of the build I’m using a GraphQL query to assemble a list of redirects and then dumping them to a JSON file, which is then being loaded by middleware.js.

Middleware Matcher

If you look at the function logs on Vercel then you’ll notice that by default all requests are being processed by the middleware. This is likely an unnecessary waste of resources because the majority of requests should bypass the middlware altogether. You can, however, apply one of more filters which will limit the paths to which the middleware is applied.

Define a config object in middlware.js. The matcher key is either a string or a list of strings that define path patterns processed by the middleware. These strings will be interpreted as REGEX. If a path does not match one of these patterns then it will not be considered by the middleware. So, for example, the /ignore/ path, although specified in the redirects.json file, will not be forwarded since it will not be processed by the middleware.

export const config = {
    matcher: ["/what-is-(.*)", "/blog/", "/latest/"],
};

Middleware Caching

If you’re processing a large number of requests (🎉 your site is busy) or there are lots of potential redirects then the middleware can add a small bit of latency to each request. Probably not perceptible to the majority of site visitors, but still something that can be mitigated to improve site performance. Unless the responses from your middleware are highly dynamic it makes sense to take advantage of edge caching to make your site more responsive.

Caching can be activated by including a Cache-Control header in the middleware response. The Cache-Control header specifies two directives:

  • s-maxage=86400 — cache a specific response for 86400 seconds (1 day); and
  • stale-while-revalidate — cache updated in the background and stale value server until fresh value available.

The screenshot below illustrates the magnitude of the potential improvement. The initial request, which was processed by the middleware, took 283 milliseconds to run. The subsequent request for the same page was served from the cache and took only 36 milliseconds.

Caching significantly reduces the server response time.

Using Request Properties

We have only considered redirecting based on the URL pathname. However, the Request object passed to the middleware has numerous other attributes which can be useful for determining where a redirect should go. These are some of the fields which I find more interesting and useful:

  • url (obviously!)
  • geo (geographic origin derived from IP address; a custom Vercel attribute)
  • method — the HTTP request method
  • headers
  • redirect — the redirect mode; and
  • referrer — the URL of the referring document.

The geo attribute in particular can be useful and has city, region and country fields.

if (req.geo.country === 'US') {
  // Do something specific for requests from the US.
}

Conclusion

Edge Middleware on Vercel is a flexible and performant way to handle redirects. If static redirects specified in vercel.json don’t quite cut the mustard for you, then check out middlware redirects as a powerful alternative.

🚀 TL;DR Show me the code. Look at the 22-vercel-redirect-middleware branch. This site is deployed here.