Exclude the crawlers from Google & Co. via “nofollow” so that your HTML preview templates do not appear in search engines.
To pass the HTML templates to the image generator later, you next create a list of all HTML templates and their paths. Here is an excerpt from my JSON file:
[
{
"filename" : "import-tweets-from-twitter-api-in-11ty-en-preview",
"path" : "https://www.d-hagemeier.com/assets/preview-images/import-tweets-from-twitter-api-in-11ty-en-preview.html"
},{
"filename" : "from-wordpress-to-jamstack-en-preview",
"path" : "https://www.d-hagemeier.com/assets/preview-images/from-wordpress-to-jamstack-en-preview.html"
},{
"filename" : "5-tips-you-can-learn-in-las-vegas-for-your-business-en-preview",
"path" : "https://www.d-hagemeier.com/assets/preview-images/5-tips-you-can-learn-in-las-vegas-for-your-business-en-preview.html"
}
]
Netlify has a big disadvantage: With every deploy the old data is deleted. There are tricks with undocumented cache directories, but I didn’t want to rely on them.
Netlify would normaly delete and recreate all image data for each deploy. Depending on how many blog articles you write and how many images are generated, this generates a lot of work.
Instead, I decided to store the thumbnails in the Google Storage. Google Storage belongs to the Google Cloud Platform, offers the storage of data in so-called buckets and is free of charge for the first 12 months.
The creation of a suitable bucket is easy after login, I have attached my personal settings in brackets:
Once the settings are done, your new bucket is waiting for you and you could already upload files manually.
To let our script store files in the bucket later, we need the corresponding Google Credentials. Just follow the official Google instructions and create a new service account. You will then receive a JSON file with your access keys. Save these keys well, they will only be generated once per service account!
Save the values CLOUD_PROJECT_ID
, BUCKET_NAME
, CLIENT_EMAIL
and PRIVATE_KEY
as .env variables, so they do not appear publicly.
Time for our actual script, in my case I called this previewimages.js. First you add the required NPM packages…
yarn add axios puppeteer @google-cloud/storage dotenv
…and register them in the script:
const axios = require('axios');
const puppeteer = require('puppeteer');
const { Storage } = require('@google-cloud/storage');
require('dotenv').config()
Next, add your variables.
If the error
error:0906D06C:PEM routines:PEM_read_bio:no start line
appears in Netlify later, my solution was to encodePRIVATEKEY
to base64. In the local development environment I had no problems, it seems to be due to the multi-line format of the key.
const GOOGLE_CLOUD_PROJECT_ID = process.env.GOOGLE_CLOUD_PROJECT_ID;
const BUCKET_NAME = process.env.GOOGLE_BUCKET_NAME;
const CLIENTEMAIL = process.env.GOOGLE_CLIENT_EMAIL;
// If you have encoded your private key using base64:
const PRIVATEKEY = Buffer.from(process.env.GOOGLE_PRIVATE_KEY, 'base64').toString();
// If not:
const PRIVATEKEY = process.env.GOOGLE_PRIVATE_KEY;
const credentials = {
client_email : CLIENTEMAIL,
private_key : PRIVATEKEY
}
And last but not least you deposit the basic settings:
const settings = {
source: "https://PATH-TO-YOUR-JSON-FILE.json",
imgwidth: 1200,
imgheight: 628
}
First, you load your JSON file via Axios and pass the data to your processing function.
axios.get(settings.source)
.then((response) => {
setupGoogleStorage(response.data);
})
.catch((err) => {
console.log('Error Axios: ', err)
});
To prevent existing thumbnails from being recreated, first check which images are already stored in the bucket.
Create a new function setupGoogleStorage
and authorize access to your bucket. Then we loop through the HTML template links and check via file.exists()
if the image is available.
If the picture exists, only a short message appears in the console. If it has to be created, you pass the path, file and filename to the get
function.
async function setupGoogleStorage(response) {
try {
const storage = new Storage({
projectId: GOOGLE_CLOUD_PROJECT_ID,
credentials: credentials
});
const bucket = storage.bucket(BUCKET_NAME);
var i;
for (i = 0; i < response.length; i++) {
let filename = response[i].filename;
let path = response[i].path;
let file = bucket.file(filename + ".png");
let exists = await file.exists().then(function(data) { return data[0]; });
if(exists == true) {
console.log("Image already exists: " + filename + ".png")
} else {
await get(path, file, filename)
}
}
} catch (err) {
console.log("Error setupGoogleStorage: ", err);
}
}
Now you actualy take the screenshots. In the get
function we start a new puppeteer page and request the screenshot via the getscreen
function.
async function get(path, file, filename) {
browser = await puppeteer.launch({ headless: true });
page = await browser.newPage();
const buffer = await getscreen(path, filename);
await uploadBuffer(file, buffer, filename)
console.log("Uploaded: " + filename + ".png")
await file.makePublic();
browser.close();
}
async function getscreen(url, filename) {
try {
console.log("Getting: " + url);
await page.setViewport({ width: settings.imgwidth, height: settings.imgheight });
await page.goto(url, { waitUntil: 'networkidle0' });
const buffer = await page.screenshot();
console.log("Got: " + filename + ".png");
return buffer;
}
catch (err) {
console.log('Error getscreen:', err);
}
}
With
networkidle0
Puppeteer waits until there is no more network activity within 500ms. This delays the creation, but also ensures that all CSS files, fonts and images are loaded correctly.
Puppeteer don’t got variables for pagescreenshot
in getscreen
and saves the screenshot only as a buffer. Now pass this buffer to the Google Bucket:
async function uploadBuffer(file, buffer, filename) {
return new Promise((resolve) => {
file.save(buffer, { destination: filename }, () => {
resolve();
});
})
}
const axios = require('axios');
const puppeteer = require('puppeteer');
const { Storage } = require('@google-cloud/storage');
const fs = require('fs');
require('dotenv').config()
const GOOGLE_CLOUD_PROJECT_ID = process.env.GOOGLE_CLOUD_PROJECT_ID;
const BUCKET_NAME = process.env.GOOGLE_BUCKET_NAME;
const CLIENTEMAIL = process.env.GOOGLE_CLIENT_EMAIL;
const PRIVATEKEY = Buffer.from(process.env.GOOGLE_PRIVATE_KEY, 'base64').toString();
const credentials = {
client_email : CLIENTEMAIL,
private_key : PRIVATEKEY
}
const settings = {
source: "https://PATH-TO-YOUR-JSON-FILE.json",
imgwidth: 1200,
imgheight: 628
}
async function setupGoogleStorage(response) {
try {
const storage = new Storage({
projectId: GOOGLE_CLOUD_PROJECT_ID,
credentials: credentials
});
const bucket = storage.bucket(BUCKET_NAME);
var i;
for (i = 0; i < response.length; i++) {
let filename = response[i].filename;
let path = response[i].path;
let file = bucket.file(filename + ".png");
let exists = await file.exists().then(function(data) { return data[0]; });
if(exists == true) {
console.log("Image already exists: " + filename + ".png")
} else {
await get(path, file, filename)
}
}
} catch (err) {
console.log("Error setupGoogleStorage: ", err);
}
}
async function get(path, file, filename) {
browser = await puppeteer.launch({ headless: true });
page = await browser.newPage();
const buffer = await getscreen(path, filename);
await uploadBuffer(file, buffer, filename)
console.log("Uploaded: " + filename + ".png")
await file.makePublic();
browser.close();
}
async function getscreen(url, filename) {
try {
console.log("Getting: " + url);
await page.setViewport({ width: settings.imgwidth, height: settings.imgheight });
await page.goto(url, { waitUntil: 'networkidle0' });
const buffer = await page.screenshot();
console.log("Got: " + filename + ".png");
return buffer;
}
catch (err) {
console.log('Error getscreen:', err);
}
}
async function uploadBuffer(file, buffer, filename) {
return new Promise((resolve) => {
file.save(buffer, { destination: filename }, () => {
resolve();
});
})
}
axios.get(settings.source)
.then((response) => {
setupGoogleStorage(response.data);
})
.catch((err) => {
console.log('Error Axios: ', err)
});
You need the appropriate metatags to display social media preview images. There are the general Open-Graph-Tags and the Twitter-Tags, both belong to <head>
of your website:
<meta property="og:image" content="https://URL-TO-YOUR-PREVIEW-IMAGE.png" />
<meta property="og:image:height" content="1200" />
<meta property="og:image:width" content="628" />
<meta property="og:image:alt" content="ALT TEXT FOR YOUR PREVIEW IMAGE" />
<meta name="twitter:image" content="https://URL-TO-YOUR-PREVIEW-IMAGE.png" />
<meta property="twitter:image:alt" content="ALT TEXT FOR YOUR PREVIEW IMAGE" />
The URL for your image is https://storage.cloud.google.com/YOUR_BUCKETNAME/IMAGENAME.png
.
In order for your large image to appear on Twitter, you also need to add an additional specification…
<meta name="twitter:card" content="summary_large_image" />
…and test the result in the Validator:
In order for each new article to receive a preview image directly, all you have to do is specify when the deploy should start. My own workflow for that:
If you don’t use Netlify, you can trigger the webhook differently. For example, if you’d like to trigger a deploy on every new article in WordPress, then add one of the automatically generated RSS feeds to IFTTT with the action “Webhook” and the Webhook target of your preview page.
That’s it, happy previewing! :D