Adding social previews to a React app without SSR

If I were to ask you: What are the main drawbacks of using a SPA? Then you will most likely answer: Bad SEO, & No social previews (as in the image above).

And for some websites, these drawbacks are game breaking... So that's why they move to frameworks like NextJs or Gatsby which solve these problems by moving the rendering to the server (SSR).

So SSR frameworks are great and all, but...

There are several times where you might prefer/need to build your app as a SPA, AND at the same time, you need good SEO & social previews for it.

So are you out of luck here?
Fortunately, you are not.

For the first point (Bad SEO), it used to be true in the past, but in recent years, web crawlers have improved a lot & they are now capable of downloading, parsing, & rendering javascript powered apps with no problem!
Gotta admit though that it's not as good as SSR apps, but still, that's much better than no indexing at all.

Now for the second part (no social previews), this is still a problem unfortunately.
Because even if your app were to insert this social media tags as soon as it renders the page (using React-Helmet for example), the crawlers that look for these preview tags don’t wait for them to load most of the times, so you end up with links that don’t have any title/description/image associated with them like this: Link without previews

Whereas what it's expected to look is: Link with previews

So is there no solution to this?!

I have suffered myself from this problem many times, & I searched a lot for some way/hack to solve it, but the only solution I found was: Have your server which is responsible for serving the app's files intercept the “GET index.html” request & dynamically insert the relevant meta tags. (See this nice article for details)

While this solution certainly works, however...
Many apps nowadays put the API & the frontend app on different hostings.
Your backend API might be on your own VPS server, but your frontend app is mostly on a service like Netlify or Vercel (because of the awesome features they have).
So in these cases, the solution above can't be implemented… 😞

But...
In the past week, I've been reading about Netlify Edge functions.
If you haven't heard about Edge functions before, then put simply, they are functions that can intercept a request made from a browser to your Netlify hosting & modify the request/response as you wish (very similar to middleware functions).
You can read more about'em here: Netlify Edge Functions explained

So reading about them & looking at their use cases, I thought to myself: Why not use Edge functions to insert the relevant meta tags in a page before it's delivered to the browser too!

And that's what I went on to do.

First, we need to specify which paths the Edge function will intercept & run on.
In my case, I only needed social previews on individual posts' pages, which had a URL structure: /post/{id}
So I specified my edge function path as: /post/*

Then, in my index.html file, I need to put some placeholders that can be replaced later by the edge function with the dynamic data, so I opened my index.html file & put the following:

<meta name="og:title" content="__META_TITLE__"/>
<meta name="og:description" content="__META_DESCRIPTION__"/>
<meta name="og:image" content="__META_IMAGE__"/>

The "__META_TITLE__" part is the placeholder that will be replaced later.

Finally, I created an edge function, & inside this function I do the following five steps:

1- Get the page that is supposed to be returned as the response

const response = await context.next();
const page = await response.text();

2- Extract the id from the url of the request

const url = new URL(request.url)
const id = url.pathname.slice(...);

3- Do a fetch request to get the meta data of this post with this id

const postData = await fetch(`my-own-api/get-post-by-id/${id}`)
        .then(res => res.json())

4- Searh & replace the placeholders in the page with the actual data that I got from the api

const updatedPage = page
        .replace('__META_TITLE__', postData.title)
        .replace('__META_DESCRIPTION__', postData.description)
        .replace('__META_IMAGE__', postData.image)

5- Return the updatedPage in the response

return new Response(updatedPage, response)

And we should be done!!

Now just to make sure we did everything right, we can make use of a useful online website
Where you just put a URL of a page, & if it has the proper meta tags, then it will show you the preview cards for this page.
Image from opengraph

You might want to ask me now:

"But what if my website isn't hosted on Netlify?? What if it's hosted on Vercel or AWS or Cloudflare Pages...etc??"

Then you'll be glad to know that most of these hosting providers now provide some kind of Edge functions that could be used to achieve the same results!
(Vercel Edge functions, Cloudflare Functions, Lambda@Edge functions,...etc)

One final small thing that might be worth mentioning:
You probably already noticed that I’m running this edge function only on the requests that are made to /post/* paths, not everything.
Although it's possible to put run the edge function on all the paths (ie /*), & check the URL inside the edge function, but I wouldn't recommend doing so.
Because this will trigger a lot of unnecessary invocations to this edge functions (which costs you money 😅)
So the higher specificity the better 👍.

And that should be everything folks!

This problem really bugged me for a lot of time, so when I finally managed to find a working solution, I wanted to share it with you right away, so hopefully you found it helpful.

Until next time,
And have a great day! 👋

My Photo

About Me

I'm a freelance web developer who specializes in frontend.
I have a special love for React. And my personal goal is: Building things that are Awesome! ✨
If you are someone who is building a project where high quality is a MUST, then Let's Chat! I'll be glad to help you with it.

© 2023-present Mohammed Taher Ghazal. All Rights Reserved.