Dynamic Social Previews for Your SPA With Functions and HTMLRewriter

A couple of weeks ago, we wanted to implement Social Previews into a Single Page Web Application while still having the capability to update the shown preview images based on the query string or route people were visiting.

Essentially, we wanted to display different images for both urls:

Social Previews

First, we had to learn about the Open Graph Protocol, and that Open Graph is the solution to share images across various social media channels and have them present nicely with an image for the link you’re sharing.

You’ve probably seen this on Facebook, Telegram, Twitter and Discord and Slack. Here’s an example of such a preview when shared through iMessage.

When such a link is shared, the platforms crawler go out there and parse the html, looking for the Open Graph Tags and put the information into the link you’re sharing. Like a preview image, a description of the page and more stuff. Like the whole tweet for instance.

In the Open Graph Protocol, you can specify tons of options how to share and display information to your users. Often, it might be just enough to put in a generic image into your sites HTML code and use this across the various channels. However, we wanted to share an image of the currently zoomed in part of the map. It’s about the details, right?

To make this happen, we had to dynamically update the og:imgage meta tag:

<meta property="og:image" content="https://example.com/please-be-dynamic" />

With a Single Page Application, this tag could only be altered using client-side JavaScript, but when crawlers parse this tag they don’t execute any JavaScript and just see the generic image or link you put in there.

Make the Image (and everything else) Property Dynamic

Cloudflare Pages functions to the rescue! Cloudflare Pages allow to dynamically run JavaScript on the edge to alter your HTML output to the user. In the background they execute like Cloudflare Workers functions, but are integrated directly into Pages and can be part of your repository without any extra configuration.

All we need to make use of, is the HTMLRewriter and attach our new meta tags to the head element of the page.

Here’s an example: This is your head of the HTML page you deploy to Cloudflare Pages or any other platform:

<head>
<title>my awesome title</title>
<link rel="icon" type="image/png" href="favicon.ico" />
</head>

Now, we add in a function to alter the output and add our Open Graph meta tags to get this output instead:

<head>
<title>my awesome title</title>
<link rel="icon" type="image/png" href="favicon.ico" />

<meta property="og:url" content="https://example/?latLonZ=53.559035%2C10.005051%2C10.00" />
<meta property="og:image" content="https://example.com/?latLonZ=53.559035,10.005051,10.00CC" />
<meta name="twitter:card" content="summary_large_image" />
</head>

See the difference after the favicon.ico line? These lines were all added dynamically and are updated for each and every query string the user is navigating to. Since this is being injected on the server side and not with client-side JavaScript, Discord and Twitter crawlers can parse and read this page and display the correct image.

HTMLRewriter

To use HTMLRewriter (and much more things) with Cloudflare Pages, you just need to create a folder called functions into the root of your project and put a file with the name _middleware.js in there. It'll be triggered for all requests. There's much more these functions can do, but we're focusing on one thing today.

This is how your project directory could look like:

README.md
functions/_middleware.js
public/index.html
src/

You can find an example repository hosted on GitHub here.. Here’s the gist right away:

let name
let ogtag

class ElementHandler {
element(element) {
element.append(ogtag, { html: true })
}
}
const rewriter = new HTMLRewriter().on("head", new ElementHandler())

export async function onRequest(context) {
const {
request, // same as existing Worker API
env, // same as existing Worker API
params, // if filename includes [id] or [[path]]
waitUntil, // same as ctx.waitUntil in existing Worker API
next, // used for middleware or to fetch assets
data, // arbitrary space for passing data between middlewares
} = context

let res = await next()

// get query strings provided with request and path name accessing the page
const { searchParams, pathname } = new URL(request.url)

// querystring I'm looking for to create dynamic images, this is an optional thing depending on your case
name = searchParams.get("myQuery")

// these are the metatags we want to inject into the site
ogtag = `
<meta property="og:title" content="my title" />
<meta property="og:description" content="my awesome project description" />
<meta property="og:type" content="website" />
<meta property="og:url" content="${request.url}" />
<meta property="og:image" content="https://example.com/preview.png?${name ? "myQuery=" + name : "default"}" />

<meta name="twitter:title" content="my twitter title" />
<meta name="twitter:description" content="my awesome description for twitter" />

<meta name="description" content="and even more stuff about my page" />
`

return rewriter.transform(res)
}