Fair warning: this is super cool.

I think WordPress, and, in particular, the block editor, is absolutely perfect for crafting landing pages. You know, marketing pages that show off features, speak to different markets, or just generally give you lots of screen real estate to deliver a message.

This is a landing page.

Here on CodePen, we have a WordPress site we use for this very blog you are reading, but also our podcast and documentation. WordPress has been very useful for us for building and maintaining these things over the long haul.

But our WordPress install, rather on purpose, is hosted at blog.codepen.io. It’s on an entirely different server than our main site at codepen.io. So we have a mismatch here. We would really prefer our landing pages to be built and maintained in WordPress, but shown on our main site. A real conundrum. This isn’t a new problem though. Grabbing content from one place and showing it on another is a classic web thing. We’ll get to this in a minute.

So since we were very sure we wanted to build and maintain these pages in WordPress, we built a Custom Post Type for them:

Landing Pages are their own special type of post in our WordPress.

I won’t get into all this in this post, but we really spiffed up the editing experience for the block editor on these landing page posts:

  1. Enqueued custom CSS so that the post inside the editor looks 95% like it does on the live site.
  2. Using our own CodePen Embed Block plugin
  3. Crafted some of our own custom blocks, including some ACF blocks
  4. Crafted some lightweight blocks like some block variations on columns.

The end result is a rather neat editing experience where your inside the CMS but editing essentially exactly what you see.

It’s almost unsettling to actually be doing WYSIWYG and have it not suck.

When we publish a page like that, it ends up at some URL on blog.codepen.io, which remember isn’t what we want. It’s fine that it exists there, but we don’t really wanna link to it there or send people there. We want to snag the content from there and display it over on codepen.io.

So how do we do this content-snagging? Well heck, we could just do it on the client-side. I remember back in the jQuery glory days you could do it in a one-liner where you tell it to load some content from a location and can even pass a selector to scope the content to just what you need.

$("#content").load("ajax/content.html #main");

So in our case we’d fetch from like https://blog.codepen.io/location/of/landing_page and plop that content into some otherwise-empty container.

But client-side content fetching? Ehhhhh. Couple issues with that:

  1. It’s slow.
  2. The perceived performance is extra slow, since the wrapper for the page would render first then the the content would come in later, causing content shifting awkwardness.
  3. Google says content shifting (“Cumulative Layout Shift”) is bad for SEO
  4. Google says JavaScript-rendered content can take a week or so to index and re-index.

We gotta get this thing server-side!

One possibility is to have some server-side language on our main website do the content fetching. I don’t hate that. Particularly if it’s cached and has good error handling and stuff.

But we’ve got a cooler solution in place. This is where the Cloudflare Workers come into play. Cloudflare Workers are capable of doing a variety of neat stuff, and it all happens “at the edge” as they say. Because our traffic at CodePen goes through Cloudflare CDN servers, that’s an opportunity to do work if we need to before the requests even get to our servers, and an opportunity to manipulate the responses on the way out if we want to. We do all this with Node JavaScript! They are like cloud functions, but again they have the ability to intercept and manipulate all the requests to a site.

For this particular idea, the feature of Cloudflare Workers we’re using it HTMLRewriter. Here’s the concept:

  1. On any request that comes to CodePen at the URL path features/*, execute the Worker. For example, features/pro
  2. If the request matches, the Worker intercepts the request and does these jobs:
    1. Goes to get the “shell” landing page we have set up on the main site
    2. Goes to get the content that should be rendering there from the WordPress site. It knows which content to get from looking at the URL.
    3. Combine the shell and the content together
    4. Return all that in the response
  3. If the request doesn’t match, just send it on through to the main site untouched.

Alex jokingly referred to this as iframe stitching. Because it’s kinda like you have a parent page with a content-only iframe in the middle of it, but there is no iframe the content is really there.

That shell page? That’s what we render at that URL on our main site assuming the Worker never ran at all. So it’s just got any empty <div> there waiting to be filled with content.

The point of the shell is that we can run our React app out there, rendering the header, sidebar, footer, user menu, and all that like the rest of our app does.

The content? How do we get that? We don’t actually wanna Ajax it from the published URL, only because that comes along with some cruft (e.g. it’s own “shell”) that we don’t need. We like the WordPress-side published URL there for preview purposes, but it doesn’t need to be the source of content.

Fortunately WordPress has a built-in REST API we can easily hit for the data. For example:


Which spits out JSON:

So basically data.content.rendered is the HTML we need that the WordPress block editor produced. And if we load up the same CSS we load up on the WordPress side to style it (we do), we’ve got a 1-to-1 match for a server-side rendered landing page!

The code for the Worker is something like…

async function handleRequest(url) {
  // Fetch the shell page from the original URL
  const shellPage = await fetch(url);
  // Fetch the content from the WordPress REST API
  const wordPressContent = await fetchWordpressContent(url);

  // Merge the two together with HTMLRewriter as the response
  return new HTMLRewriter()
    .on('#cloudflare-worker-content', new ElementHandler(wordPressContent))

Is it slow, you ask? Slow to intercept a request, have it make two requests and stitch them together before returning? Technically, it’s slower than the single original request would have been. But we’re talking low single-digit milliseconds here:

This isn’t the same ballpark is a client-side fetch, where the users own network is needed to make it all happen. These are massive edge servers talking to other massive edge servers and doing who-knows-what behind the scenes to make it lightning quick.

Once we cracked the nut with Cloudflare Workers, we start seeing the potential in them everywhere. For example, all our image assets (our own, and yours) are served through Workers, which allow us to do stuff like manipulate them on the fly.