I recently started to redesign every JSMonday featured image using a new format (the one you’re seeing in the above image). I needed to create 60 new images for 20 articles by hand. That’s 3 images per article — the Open Graph, Instagram story, and Instagram post images.

How great would it be to generate the new article image set by simply making it a REST request and sending the title, subtitle, and the background image?

Let’s see how to do so by setting up a simple project using React and Puppeteer.

First, let’s divide our project between the React app (the UI we build of the image + text) and the image render script, we want a folder structure like this:

[root]
  |
  +----[react]
  |
  +----[render]

Let’s get started by creating a new React app with create-react-app:

$ npx create-react-app react-project

Great! Now let’s think about which kind of image we want to create.
For this article, I’d like to create a FullHD (1920*1080px) image with some text on it. Let’s design it just to have a reference during development:

Puppeteer

As you can see we’re using a Google Font (Roboto) and an image from Unsplash, so we’ll need to render them inside our components.

Let’s start by creating our background component. Please note that a clean React architecture is not a goal for this article. We just need to create something that works and render it as an image!

The Background Component

import React from "react";
import "./Background.css";

export default function background(props) {
  return (
    <div className="background" style={{ backgroundImage: `url(${props.image})` }}>
      { props.children }
    </div>
  )
}
.background {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 1920px;
  height: 1080px;
  background-size: cover;
  background-position: center;
}

As you can see, we’re just creating an easy component which will take the background image from props and will render all the content passed as a children prop.

The Title Component

import React from "react";
import "./Title.css";

export default function title(props) {
  return (
    <h1 className="title">  { props.text } </h1>
  )
}
.title {
  margin: 0;
  color: #fff;
  font-size: 144px;
  font-weight: 900;
}

The Title component is even simpler — it takes a prop called text and will render it inside an h1 tag.

The Subtitle Component

import React from "react";
import "./Subtitle.css";

export default function subtitle(props) {
  return (
    <div className="subtitle">
      { props.text }
    </div>
  )
}
.background {
  color: #fff;
  font-size: 48px;
  margin-top: 5px;
}

Just like the Title component, Subtitle will take a prop called text and will render it inside a div. Pretty simple!

The App Component

import React      from "react";
import Background from "./components/Background";
import Title      from "./components/Title";
import Subtitle   from "./components/Subtitle";
import "./App.css";

const image = "https://images.unsplash.com/photo-1555448248-2571daf6344b?w=1920&q=100";

export default function App() {
  return (
    <Background image={image}>
      <Title text="JSMonday Rocks" />
      <Subtitle text="Let's generate a beautiful image out of this React scene" />
    </Background>
  )
}
@import url('https://fonts.googleapis.com/css?family=Roboto;400,900&display=swap');

* {
  font-family: 'Roboto', sans-serif;
}

With the App component, we’re mounting the entire image and text. Let’s see the result in a web browser:

ReactJS and Puppeteer

Awesome! That’s exactly what we need.

Rendering the React Project as a PNG File

We need to create a production build of our React app. Let’s add the "homepage” property to our package.json file. This will make it work locally just by opening the generated index.html file with a browser:

{
  ...
  "homepage": "./",
  ...
}

now let’s launch the build using the default create-react-app scripts:

$yarn build

Great! Now we have the following folder structure:

[root]
  |
  +---[react-project]
  |   |
  |   +--[build]
  |      |
  |      +-- index.html
  |      |
  |      +-- [static]
  |
  +---[render-project]
  |   |
  |   +-- index.js

We’ve separated the React app project from the Render project (the one which will generate the image for us). Inside the /react-project/build directory, we can find the production build that we’ll use for generating our image.

Now let’s move inside our render-project folder and initialize a new package.json file.

$ yarn init -y

Now let’s add the only dependency we need, Puppeteer.

$ yarn add puppeteer

Puppeteer is an amazing library built by Google which exposes headless Chrome APIs. That means that most everything we can do on Google Chrome can be done using this amazing library!

Let’s see how to implement the image generation:

const puppeteer = require("puppeteer");

async function generateImage() {
  const browser = await puppeteer.launch();
  const page    = await browser.newPage();

  await page.goto(`file://${__dirname}/../react-project/build/index.html`);
  await page.setViewport({ width: 1920, height: 1080 });
  await page.screenshot({ path: "./myAwesomeImage.png" });
  await browser.close();
}

And that’s it! Just 14 lines of code that will generate a PNG image from our React scene!

Let’s analyze in depth what we’re doing:

  1. We create a new Puppeteer browser.
  2. We create a new page.
  3. We set its destination URL to our generated index.html file. Please note that we’re using file://, http protocol won’t work locally for this specific case!
  4. We set the viewport to 1920x1080 pixels. That way we’ll cover the entire React scene and we’ll capture it whole with the next script.
  5. We take the screenshot of the page and save it to ./myAwesomeImage.png file.
  6. We close the browser.

And now let’s see the result:

Can you guess which one is the original image and which one has been generated with React? Me neither!

You can find the full project in this repository:
github.com/jsmonday/jsm22

Next Steps

So now, what can we do next? I’ve just open sourced the code for a Google Cloud Function (it would also work as a AWS Lambda) which generates Instagram post/stories and Open Graph images just by sending title, subtitle, and image link over a REST API: github.com/jsmonday/sigf

The possibilities are endless. You could build an Express server which generates images based on certain parameters, or you could also generate PDF invoices using Puppeteer and React. It’s all up to your imagination!

Did you like this article? Consider becoming a Patron!

This article is CC0 1.0 (Public Domain) licensed.