Andrew
Walpole

Principal Web
Developer &
Engineering
Manager

Designer
Leader
Teacher
Learner
Maker

Blog Post Image Generator with HTML2Canvas

I’m a big fan of the HTML2Canvas library. It has some quirks and is less maintained (lots of issues and open PRs) than I would like to see – actually I worry quite a bit about it becoming too stale to use, just because it can be so useful!

Here’s a look at one way I’m using it to generate preview images for my blog posts, it’s so simple, my production version of it just lives in codepen.

Before I get to that, I just want to clarify that this is not automatic social share images. Though this could be a piece of getting there, the biggest issue is that a hands-off solution will require you to automate the “run in browser” portion of an automated process, which you can pull off with something like puppeteer, but that’s not in scope of this post.

The Setup

The setup is mostly focused on getting some HTML and CSS up that I would like to render as an image.

<div class="featured-image" :class="{ square: isSquare }">
    <h1>{{title}}</h1>
    <p>{{summary}}</p>
    <div class="site-tag">
      <svg viewBox="0 0 1158 1201" fill="none" xmlns="http://www.w3.org/2000/svg">
        <!-- truncated for space -->
      </svg>
      <div class="tag-content">
        <p>Andrew Walpole &copy; {{year}}</p>
        <p>andrewwalpole.com</p>
      </div>
    </div>

You’ll notice some {{petite-vue}} content placeholders here, but it’s not required. You could hardcode the content in, or use vanilla JavaScript to update it. But for me, petite-vue is a quick way to hook the content up to text inputs that I can easily use to generate a new custom image.

Here are those form fields getting hooked up to the content:

<label>
  Post Title:
  <input type="text" v-model="title">
</label>
<label>
  Post Summary:
  <textarea v-model="summary" cols=50 rows=5></textarea>
</label>
<label>
  <input type="checkbox" v-model="isSquare"> Square
</label>
<button @click="saveDown">Save Image</button>

As mentioned, this provides a simple interface to allow the content to be customized quickly. The title and description can be entered, and I even have an option to toggle between wide and square layouts.

The Secret Sauce

The last but most special bit is to use html2canvas to generate an image from the html, and it’s pretty easy:

import "https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js";

saveDown() {
  const containerToRender = document.querySelector('.featured-image');
  html2canvas(
    containerToRender,
    { 
      allowTaint: true, 
      useCORS: true, 
      backgroundColor: "transparent",  
      width: containerToRender.offsetWidth, 
      height: containerToRender.offsetHeight,
    }
  ).then(canvas => {
    const previewArea = document.querySelector('.preview-area');
    previewArea.innerHTML = "";
    previewArea.appendChild(canvas);
  });
}

We querySelector for the container we want to imagify, then call html2canvas( <container>, <options> ). It returns a promise that when fulfilled will give you back a <canvas> element. We can then just append that canvas into our HTML and you can right click it to Save Image As....

A few things to note. We’re using the container’s width and height to render the image, this allows our size toggle to work, and will also render a different image if the browser window is constraining the size. You can fix these values if you want a consistent size. Also, I had some issues with the SVG positioning and the opacity settings on it, also the ::before on the title renders oddly the first time you save it, and then correctly the second time. These are those quirks I mentioned at the beginning.

The image that is generated is a PNG, which is not well optimized. I find myself needing to run it through squoosh before using it, and that’s a little annoying, but in my search for a client-side optimizer that could eliminate this step I have come up short.

Finally here’s the whole thing for you to see and play with:

See the Pen Blog Post Featured Image Generator by Andrew (@walpolea) on CodePen.