Dynamic Gatsby Routes on Cloudflare Pages

This post is about getting dynamic routes – or more specifically the rewrites for dynamic routes – to work with Cloudflare Pages deployments.

Cloudflare Pages is still quite new and thus not as feature rich as other longer established JAMstack platforms. But what it lacks in features, it makes up in speed, which is why I desperately want to move some of my sites to it.

As an example in this post I'll be using an actual route I am using in one of my shops. It is a dynamic route that allows my customers to check the status of their order. The route has the following format:

/status/:orderId/:token

It contains the id of the order and a order-specific secret token to restrict access to this page to the ones who know the URL (only the customer). This page can obviously not be generated during built-time of the site. It instead has to fetch the current order state on the client-side based on the order id and token from the URL.

Since the shop in question is built in Gatsby, I'll be using actual code examples from Gatsby. But the approaches shown in this post can also be applied to (exported) Next.js and other static sites. In Gatsby, I am creating the dynamic route as follows:

createPage({
  path: '/status/:orderId/:token',
  matchPath: '/status/:orderId/:token',
  component: path.resolve('./src/templates/status.tsx'),
})

This creates the static /status/:orderId/:token.html when the site is built. We now have to make sure that all URLs that match /status/:orderId/:token (e.g. /status/1234/EvEf_HAp7oyb194ZekX39bIcwueV1) are rewritten to /status/:orderId/:token.html. We need a rewrite and not a redirect, because a redirect would change the URL in the browser's address bar and would also lose the actual order id and the token. While Gatsby would still be able to figure out that it has to load /status/:orderId/:token/index.html if not have the rewrite, it'd first show a 404 before doing a client-side redirect to the correct page. This is a bad UX and needs to be avoided.

On Netlify, I am simply using a rewrite in the _redirects file right now:

/status/*  /status/:orderId/:token/index.html  200

The status of 200 makes sure that this is a rewrite and not a redirect. While Cloudflare Pages also supports _redirects files, it only does for redirects (301 and 302) for now. We thus need another solution.

Option 0: Pages Functions

Option added to the post on the 2021-11-27.

Cloudflare released Pages Functions since this post was first created. Pages Functions are now the recommended approach. It is basically the same as Option 2: Cloudflare Worker just that it integrates nicely into your existing repository and doesn't require maintaining a separate worker.

The function for the rewrite is as simple as creating /functions/status/[order]/[token].ts. With the following content:

export function onRequestGet({
  env,
  request,
}: EventContext<Env, string, unknown>) {
  return env.ASSETS.fetch(
    new Request(
      new URL("/status/:orderId/:token/", request.url).toString(),
      request
    )
  );
}

interface Env {
  ASSETS: Fetcher;
}

Make sure that the /functions/ directory is at the root of your project (the root you specify in the Pages build settings). You don't have to transpile the function from TypeScript to JavaScript, Pages will automatically take care of it.

Option 1: Transform Rule

The first possible solution is to use a transform rule. In your Cloudflare Dashboard, select your site, go to Rules and create a new Rewrite URL transform rule. Setup the rule as shown in the screenshot below.

Cloudflare Transform Rule

Make sure to use the matches regex operator to match from the beginning of the URI path. Don't use a contains operator because we don't want to rewrite the page-data.json URL from Gatsby which is located at /page-data/status/:orderId/:token/page-data.json in the example.

Also make sure to rewrite the URL to /status/:orderId/:token/, and not directly to /status/:orderId/:token/index.html, because Cloudflare would return a redirect to /status/:orderId/:token/index in the latter case (and a rewrite to /status/:orderId/:token/ if you try to rewrite it to /status/:orderId/:token/index).

If you are still on the Free plan, you don't have access to the matches regex operator. In this case, you can fall back to option 2.

Option 2: Cloudflare Worker

As a second solution, you can deploy a very simple Cloudflare Worker. The docs will get you started on how to create and deploy a worker, so I'll only show the relevant pieces here.

The worker itself is as simple as:

addEventListener("fetch", (event) => {
  event.respondWith(
    fetch(
      new Request(
        new URL("/status/:orderId/:token/", event.request.url).toString(),
        event.request
      )
    )
  );
});

It just takes the request, modifies its URL, and executes it. The worker doesn't check if the incoming URL starts with /status, since this can be done by deploying the worker to the corresponding route instead. Just add the route setting in your wrangler.toml accordingly:

route = "example.org/status/*"

Drawbacks?

What I don't like about both solutions is that they are not tied to the repository of my site. I prefer that each commit in my repo is a self-contained deployment, i.e., I can always check out a specific commit, run one deployment command and everything works as it was supposed to at this commit.

However, I could probably achieve this with both options. For Option 1 I'd have to add calls to Cloudflare's API to ensure that the transform rule exists as intended for the commit, and for Option 2 I could add the worker to the repository and update it as part of the site's deployment.

Another drawback is that both options require your domain to go through Cloudflare - but if you are using Cloudflare Pages, chances are good that it already does.

Fixed with Option 0 that was added on the 2021-11-27.

Can I move my shop to Cloudflare Pages yet?

No, unfortunately. I am doing i18n on built-time and thus have different deployments (and domains) for each language I support. Cloudflare Pages does not support multiple projects per repository yet.


Comments: dev.to