Adding Opengraph to your single page app

The easy way to add social media link previews to a serverless web app.

13 August 2020

I love single page apps. But one perennial problem with them is that most social media crawlers from Twitter to LinkedIn cannot process the page (to generate social media previews) without server-side intervention.

The most common solution around this is to have a lightweight server side app that serves your app, but also listens for crawlers from Twitter, LinkedIn et al and serves them a barebones HTML response with Opengraph tags embedded.

Given I was working on a small/medium sized SPA, this was overkill for my purposes, and so I investigated other options. I eventually came across a library called react-snap.

React-snap is a static site generator that works by firing up a Chromium browser instance, visits your root path, and scans for all internal links. It then recursively navigates to each of those links, saves the HTML, and repeats the process.

Needless to say, this solution is only practical if you have a SPA with known or fixed routes. Dynamic URLs (like those used for usernames in social networks) would not be feasible.

index.tsx

In the entry index.tsx file of your app, you need to replace ReactDOM.render with a conditional ReactDom.hydrate statement like the following:

const rootElm = document.getElementById('root')!;
if (rootElm.hasChildNodes()) {
  ReactDOM.hydrate(app, rootElm);
} else {
  ReactDOM.render(app, rootElm);
};

The hydrate call is used when the HTML has been pre-rendered (in our case, by a static site generator, but can also be server-side) and therefore we can tell React to preserve it and only attach event handlers. Note this is only occurs on startup of the SPA - after this point, the application functions as a regular SPA.

package.json

Next, we add the following to your package.json file:

"scripts": {
  "postbuild": "react-snap"
}

And that's our static site generator setup complete! Whenever we run yarn build, we now have our static site output to the build folder.

MetaTags.jsx

Next I create a helper component to wrap around react-helmet to render all my metatags.

import React from 'react';
import { Helmet } from 'react-helmet';

type MetaTagsProps = {
  title: string;
  description: string;
  ogType?: string;
  // Relative path to your image - this component will fully qualify the URL for you
  image?: string;
};

const MetaTags: React.FC<MetaTagsProps> = (props) => {

  // Create a REACT_APP_PUBLIC_URL property in your env file which is your fully qualified domain e.g. https://www.mydomain.com
  const PUBLIC_URL = process.env.REACT_APP_PUBLIC_URL!;
  const url = PUBLIC_URL + window.location.pathname;
  const imageUrl = PUBLIC_URL + props.image;

  return (
    <Helmet>
      <title>{props.title}</title>
      <meta name="description" content={props.description} />
      <meta property="og:type" content={props.ogType} />
      <meta property="og:site_name" content="YOUR SITE NAME" />
      <meta property="og:title" content={props.title} />
      <meta property="og:description" content={props.description} />
      <meta property="og:url" content={url} />
      <meta property="og:image" content={imageUrl} />
      <meta property="og:image:width" content="1200" />
      <meta property="og:image:height" content="800" />
      <meta property="twitter:card" content="summary_large_image" />
      <meta property="twitter:site" content="@YOUR_TWITTER_HANDLE" />
      <meta property="twitter:creator" content="@YOUR_TWITTER_HANDLE" />
      <meta property="twitter:title" content={props.title} />
      <meta property="twitter:description" content={props.description} />
      <meta property="twitter:url" content={url} />
      <meta property="twitter:image" content={imageUrl} />
    </Helmet>
  );
};


MetaTags.defaultProps = {
  title: '',
  description: '',
  ogType: 'website',
  image: '/images/icons/logo-opengraph.jpg', // point to your default Opengraph image
};

export { MetaTags };

HomeRoute.tsx

All that's left to do is call the component in each of your routes.

import React from 'react';
import { MetaTags } from '_components';
import './HomeRoute.scss';
import imgBanner from './images/banner.jpg';

const HomeRoute: React.FC = (props) => {

  return (
    <div className="HomeRoute">

      <MetaTags
        title="My page title"
        description="My page description"
        image={imgBanner}
      />

      <h1>Hello World!</h1>

    </div>
  );
};

export { HomeRoute };

That wraps up the process I took to convert my SPA into a static site, with support for social media sharing previews. For anyone interested, the deployment environment for this SPA was GitHub Pages. If this helped you out, or you have any questions, drop me a line on Twitter or Instagram - I'd love to hear from you!

Recent posts

Photo Series: Dreamstate

Devon (UK) is always a great place for photography in the summer; these were taken on the southern coast, close to Exeter.

Photo Series: Indian Nights

These photos were taken on a recent holiday to India.

Photo Series: Epoch

More industrial and city photos with a vintage theme.