Improving my automated open graph image process w/ Eleventy
My first attempt at automated social sharing images with Eleventy and Puppeteer worked, but I felt that my solution was a pretty rough one. I think I've come up with a better, cleaner solution.
I used the same subpage for capturing the screenshot as before, so start there if you're building your own solution.
Puppeteer
I've seen some other solutions that are generating open graph social sharing images in a different way but I still prefer to just capture a screenshot. I want my solution (and my site!) to be self contained, without relaying on build processes on github or netlify. My preferred workflow is to generate my site then git push the updated static content up to my hosting provider.
This time around, I started from scratch without really looking at anyone else's solution. Everything starts with puppeteer. I found the documentation to be good and their examples worked perfectly for me. I created a new folder at the top level of my project called _functions
and then created a new file called og-images.js
. Here is the code I started with by plugging in one of my posts:
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('http://127.0.0.1:5500/posts/hiking-black-mountain-2021/og-image/index.html');
await page.setViewport({
width: 600,
height: 315,
deviceScaleFactor: 2
});
await page.screenshot({ path: '../obsolete29.com.v2/posts/hiking-black-mountain-2021/og-image/og-image.jpeg' });
await browser.close();
})();
Now when I run node _functions\og-images.js
, a screenshot is captured! Nice.
Iterating over my posts to capture the screenshot
Ok cool, I can capture a screenshot of a page but how do we iterate over all the posts to generate the screenshot images for all the posts? I updated the template I used for v1 of the site with some modifications. This is my updated posts-json.njk
layout file and it'll generate a file called posts.json
and place that in a new directory called _temp
. Just like in the first version of my site, I'll use this json file to iterate through all the posts to generate the screenshots.
---
permalink: _temp/posts.json
permalinkBypassOutputDir: true
eleventyExcludeFromCollections: true
---
[{% for post in collections.posts %}
{
"filepath":"{{ post.inputPath }}",
"url":"{{ post.url }}",
"socialCard":"{{ post.data.socialCardUrl }}",
"generateOpenGraphImage":"{{ post.data.generateOpenGraphImage }}"
}{% if loop.last == false %},{% endif %}
{% endfor %}]
The updated json file looks like this:
[
{
"filepath":"./src/posts/leaning-into-power-platform/index.md",
"url":"/posts/leaning-into-power-platform/",
"socialCard":"/posts/leaning-into-power-platform/og-image/og-social-cover.jpg",
"generateOpenGraphImage":"false"
},
{
"filepath":"./src/posts/hiking-black-mountain-2021/index.md",
"url":"/posts/hiking-black-mountain-2021/",
"socialCard":"/posts/hiking-black-mountain-2021/og-image/og-social-cover.jpg",
"generateOpenGraphImage":"true"
}
]
I didn't want to generate screenshots for every post, every time I built my site. I only wanted to generate the social sharing images for new posts. To accomplish this, I added a new field to the front matter for my new post template called generateOpenGraphImage
. I'll use this field as a boolean field to identify new posts. When the field is set to true, then we should generate a social sharing screenshot. If it's set to false, then skip it. Finally, I wanted the process to mark generateOpenGraphImage
as false in the markdown file so we know that a social sharing image has already been generated for this post.
The thing I struggled the most with was changing the value of generateOpenGraphImage
in the markdown file. I had it stuck in my head that I needed some sort of wrapper function to interact with front matter and/or yaml. I decided to go back to basics. These markdown files are just plain text so I should be able to do simple find and replace actions with node and javascript.
I'm new to javascript but I have several years of experience with PowerShell so I was able to hack together a basic script of basic functionality in no time by searching the web. My little script first opens the posts.json file and then iterates through all the posts. If a post's generateOpenGraphImage
is marked as true, then take the screenshot and mark generateOpenGraphImage
as false. Here's the code:
// _functions/og-images.js
const puppeteer = require('puppeteer');
fs = require('fs');
const data = fs.readFileSync('_temp/posts.json', 'utf8');
const posts = JSON.parse(data);
const localhost = 'http://127.0.0.1:5500';
const localdir = '../obsolete29.com.v2';
// iterate through each post
posts.forEach(post => {
if (post.generateOpenGraphImage == "true") {
console.log("Processing " + post.url + "...");
// generate the screenshot
(async () => {
let localPage = localhost + post.url + '/og-image/index.html'
let localImage = localdir + post.socialCard
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(localPage);
await page.setViewport({
width: 600,
height: 315,
deviceScaleFactor: 2
});
await page.screenshot({
path: localImage,
quality: 70
});
await browser.close();
})();
// update the value of generateOpenGraphImage from true to false
fs.readFile(post.filepath, 'utf8', function (err,postdata) {
if (err) {
return console.log(err);
}
let searchString = "generateOpenGraphImage: true";
let replaceString = "generateOpenGraphImage: false";
if (postdata.indexOf(searchString) > -1) {
let newValue = postdata.replace(searchString, replaceString);
fs.writeFile(post.filepath, newValue, 'utf8', function (err) {
if (err) return console.log(err);
});
}
});
}
});
Updated workflow for new posts
Now, when I'm ready to post a new post, I create my new post in markdown and run npx @11ty/eleventy --incremental
to build my html pages. Next, I run node _functions/og-images.js
to generate the open graph image for the new post. Finally, I use git to push my new post up to github. Easy peasy. :)
Ok that's it for now. Thanks for reading!
This post is part of a multi-part series on how I built my site using Eleventy. These are all the posts in the series. Check them out if that's your jam.