Last time we talked about Cloudflare Workers on this blog we talked about doing Server-Side Rendering with Svelte in a Cloudflare Worker. I also didn't share any code, which is lame. I'm sorry. It's time to right some of those wrongs though. Let's talk about React this time and see how we can use leverage Cloudflare Workers instead of running our own node proxy servers to handle Server-Side Rendering (SSR).

First, let's start at the very beginning. By default, every Sitecore JSS site that is based on any of the sample apps has a separate bundle to handle SSR. This is a stock implementation of the React server.js file. This file exports a single function called renderView. This function takes a few parameters:

  • callback - The function renderView calls after it is done performing the SSR operations for the request.
  • path - The route that is currently being rendered
  • data - A JavaScript object based off of JSON that is returned from Layout Service, this data is just a representation of the rendered page.
  • viewBag - Any additional view data used to render the page, the most commonly used object it contains is the Sitecore dictionary.

renderView is just a function and since it is exported, we can import it from the compiled bundle and call it ourselves. So for instance, this is something that is possible to do:

import { renderView } from '../build/server.bundle';

// Build parameters for renderView ... take a nap, whatever.

renderView((error, { html }) => {
     // Enjoy your rendered HTML here
}, path, layout, viewBag);

We can run this code anywhere, including a Cloudflare Worker. By doing this we will essentially be creating a small version of the SSR Proxy. The proxy, at it's very core, does this:

This is where Cloudflare Workers come into play. Workers allow you to run JavaScript on Cloudflare's edge and handle incoming requests any way you see fit. The free tier gives you 10ms of CPU time per request and that increases to 50ms on the next tier up. Check out how many neat projects are built using workers.

At first, this doesn't seem like it would give us enough time to perform all of the operations we need to do in that amount of time. The important part here is that they only count CPU time against you. Time waiting for web requests to execute will not count against us here. We really just need to be able to render components based on JSON in under 10ms (or 50ms for paid tier.) Which React can easily do.

Here is the Sitecore JSS React sample that has a full implementation of Server-Side Rendering using a worker in it. All of the steps outlined above are implemented in this worker and I've tried to comment the implementation as much as possible. A lot of the core worker code is located here but there are some helper classes in the same directory that are also used.

This worker can be deployed and used with disconnected mode, you just need to expose your disconnected layout service publicly. I did this with ngrok but there are other solutions for achieving this as well. Workers in development mode do not run on your personal computer and if you try to hit localhost from your worker, you might get rick rolled.

Application Changes

Only a few small changes were needed to allow this to work with Cloudflare Workers.

setupProxy.js was modified and a new static directory was added. This really only impacts disconnected mode but it allows your built assets to be served. Otherwise you'll have differences in bundle names after SSR generates HTML. You might not need this, there's also probably a better answer here.

app.use(config.sitecoreDistPath, express.static('build'));

We also needed some additional configuration to tell the worker what the real origin server is. So in generate-config.js, these new values are pulled from config files.

function transformScJssConfig() {
  // scjssconfig.json may not exist if you've never run setup
  // so if it doesn't we substitute a fake object
  let config;
  try {
    // eslint-disable-next-line global-require
    config = require('../scjssconfig.json');
  } catch (e) {
    return {};
  }

  if (!config) return {};

  return {
    sitecoreApiKey: config.sitecore.apiKey,
    sitecoreApiHost: config.sitecore.layoutServiceHost,
    originSitecoreApiHost: config.sitecore.originHost,
  };
}

And ...

function addGraphQLConfig(baseConfig) {
  if (!baseConfig.graphQLEndpointPath || typeof baseConfig.sitecoreApiHost === 'undefined') {
    console.error(
      'The `graphQLEndpointPath` and/or `layoutServiceHost` configurations were not defined. You may need to run `jss setup`.'
    );
    process.exit(1);
  }

  // eslint-disable-next-line no-param-reassign
  baseConfig.graphQLEndpoint = `${baseConfig.sitecoreApiHost}${
    baseConfig.graphQLEndpointPath
  }?sc_apikey=${baseConfig.sitecoreApiKey}`;

  // eslint-disable-next-line no-param-reassign
  baseConfig.originGraphQLEndpoint = `${baseConfig.originSitecoreApiHost}${
    baseConfig.graphQLEndpointPath
  }?sc_apikey=${baseConfig.sitecoreApiKey}`;
}

The ones with the "origin" prefix being the new variables that are added. These are then pulled into our worker and we swap out the values out of configuration to point back at origin instead.

config.sitecoreApiHost = config.originSitecoreApiHost;
config.graphQLEndpoint = config.originGraphQLEndpoint;

That's it though.

Working with this sample

Here's how to set this up and get started working with it:

  • Install the Sitecore JSS CLI: npm install -g @sitecore-jss/sitecore-jss-cli
  • Install Wrangler: npm install -g @cloudflare/wrangler
  • Clone the repository: git clone https://github.com/erzr/jss-react-cloudflare-worker.git
  • cd jss-react-cloudflare-worker
  • Open wrangler.toml, populate account_id and zone_id, these can be found by logging into your Cloudflare Workers account and copying the "Account ID" field in the sidebar. I used "Account ID" for both values in the wrangler.toml.
  • jss setup Run through this like you would when deploying any other Sitecore JSS site to connected mode.
  • Expose port 3000 somehow externally. I used ngrok, ngrok http 3000. Ignore this step if you deploy to a publicly accessible site already.
  • After you're connected, open scjssconfig.toml. Add a property named originHost, use the address from the step above as the value of this field. Should look like this now:
{
  "sitecore": {
    "instancePath": "C:\\inetpub\\wwwroot\\sc931sc.dev.local",
    "layoutServiceHost": "http://cfworker.dev.local",
    "deployUrl": "http://cfworker.dev.local/sitecore/api/jss/import",
    "apiKey": "{B16C4CEA-115C-4B45-BFA2-129B6EB544CD}",
    "deploySecret": "NOTASECRETIFISHAREDTHISVALUE",
    "originHost": "http://aa9ec019e36d.ngrok.io"
  }
}
  • jss start:cf

At this point two windows should pop up, your disconnected Sitecore site and the Cloudflare worker preview page. You can now test and when you're ready, deploy.

Rendering Host for Experience Editor

Yeah, this is possible to do in a worker as well. The http runtime was introduced in 9.2 and this runtime essentially just posts all the data that you would get in renderView at a URL and that URL is expected to just return the rendered page. It's not part of the GitHub implementation but this is some code I pulled from my Svelte implementation, I'll add it to the React version soon.

async function handleRenderingHostRequest(url, request) {
  if (request.method === 'POST') {
    if (!url.search || !url.search.indexOf('svelteiscool') < 0) {
      return new Response(JSON.stringify({ error: 'No permissions, sorry!' }), {headers: { "Content-Type": "application/json" }});
    }

    const invocationInfo = await request.json();

    const result = {
      layoutData: null,
      viewBag: null,
      renderPath: ''
    };

    if (!invocationInfo || !invocationInfo.args || !Array.isArray(invocationInfo.args) || invocationInfo.args.length < 3) {
      return new Response(JSON.stringify({ error: 'Invalid Request Data' }), {headers: { "Content-Type": "application/json" }});
    }

    result.renderPath = invocationInfo.args[0];
    result.layoutData = tryParseJson(invocationInfo.args[1]);
    result.viewBag  = tryParseJson(invocationInfo.args[2]);

    const renderedHtml = await renderViewAsync(result.renderPath, result.layoutData, result.viewBag);

    const responseObject = {
      html: renderedHtml,
      statusCode: 200
    }

    return new Response(JSON.stringify(responseObject), {headers: { "Content-Type": "application/json" }});

  } else if (request.method === 'GET') {
    return new Response('<div>Rendering Host: GET requests not supported.</div>', {headers: { "Content-Type": "text/html" }})
  }
}

This was adapted from Adam Weber's Gist. This is a great feature to have because it would mean that you only need to deploy your bundle in one page. Cloudflare Workers powered Experience Editor? Yes.

        <app name="svelteclean"
             sitecorePath="/sitecore/content/svelteclean"
             useLanguageSpecificLayout="true"
             graphQLEndpoint="/api/svelteclean"
			 serverSideRenderingEngine="http"
			 serverSideRenderingEngineEndpointUrl="https://silent-resonance-320e.aelamarre.workers.dev/jss-render?svelteiscool"
             inherits="defaults"
        />

Conclusion

I personally find this technique so appealing because you could get started with Sitecore JSS with no additional infrastructure needed. You don't have to worry about sizing your Node servers and managing these over time. This implementation is really just the tip of the iceberg as well. There's been no real consideration here for cache settings or other performance enhancements (store your layout service data in KV maybe?) There's a lot to still explore here but there's a lot of potential.

Please check out all of the code for this implementation in the GitHub repository.