The Case for Better Social Cards
Open Graph markup has been around long enough that most pages carry the basics:
<meta property="og:title" content="The blue sky strategy" />
<meta property="og:description" content="Less clouds, more blue" />
<meta property="og:image" content="/sky-with-clouds.jpg" />
When a link is shared on Facebook, LinkedIn, Slack, Discord, or iMessage, the platform assembles a card from those tags. Twitter has its own set of markups via Twitter Cards but falls back to Open Graph when the dedicated tags are absent.
The title and description get small treatment in those cards. The social image dominates. The visual real estate and the size of the clickable area make the image the element that gets people to notice—and click—a shared URL.
The Spectrum of Social Images
Social images broadly fall into four tiers, from not using one at all to making content-aware images for every URL. None of these are official categories, but they help frame how much effort goes into each approach.
No Image
The lowest tier simply omits the image tag entirely. The resulting link shares are small and visually flat, prone to getting lost in busy feeds.

A Single Site-wide Image
Using one image for every page on a site gets attention, but that attention is arguably negative. A single repetitive card across a stream of links from the same source feels lazy and spammy.

Per-post Featured Images
The standard practice on news and blog sites: each post gets its own image. It works well for editorial content with real photography. The downside is the overhead of sourcing or creating artwork for every new post, which often leads to filler stock images that do more to dilute a brand than build it.

Automated, Content-aware Cards
CSS-Tricks and a handful of other sites take this approach. Their cards are branded with a repeatable layout, the post title, the author’s name, and a profile picture. They are recognizable and distinct for each page, but building such a system requires a programmatic approach—creating unique images by hand for every link is not feasible.

Setting Up the Playground
To explore automated social images, we need a site to test with. An Eleventy blog is a lightweight choice. The official eleventy-base-blog starter is the quickest path to a working set of pages.

Create a repository from that template on GitHub, then clone it locally. The usual setup commands get the site running at http://localhost:8080.
git clone [your repo URL]
cd my-demo-blog ### Or whatever you named it
npm install
npm run serve
Deploy the blog to Netlify while we’re at it, since the automation for social images will rely on a Netlify Function.

Completion of the Netlify setup triggers the first deployment, and shortly thereafter the starter blog is live.

Designing the Image Template
Rendering social images on the fly usually implies headless Chrome and Puppeteer. Resoc provides an Image Template Development Kit that wraps that stack and provides a browser-based viewer for iterating on the design.
npx itdk init resoc-templates/default -m title-description
The scaffolded template in resoc-templates/default accepts a title and a description. To move beyond the basic site-wide card, the template needs to show the post title prominently, plus a branded footer with the author’s name and photo. Remove the description parameter from resoc-templates/default/resoc.manifest.json so the viewer updates its input panel.
{
"partials": {
"content": "./content.html.mustache",
"styles": "./styles.css.mustache"
},
"parameters": [
{
"name": "title",
"type": "text",
"demoValue": "A picture is worth a thousand words"
}
]
}
The viewer opens the template at the 1200×630 dimensions recommended by Facebook. The HTML lives in resoc-templates/default/content.html.mustache, using Mustache syntax to inject the parameter values.
<div class="wrapper">
<main>
<h1>{{ title }}</h1>
</main>
<footer>
<img src="profil-pic.jpg" />
<h2>Philippe Bernard</h2>
</footer>
</div>
A profile picture is needed—profil-pic.jpg should be placed in the template directory as referenced by the HTML. The stylesheet in resoc-templates/default/styles.css.mustache uses viewport-relative vw and vh units to size the layout across the wide range of contexts where these images appear.
@import url('https://fonts.googleapis.com/css2?family=Anton&family=Raleway&display=swap');
.wrapper {
display: flex;
flex-direction: column;
}
main {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
}
h1 {
text-align: right;
margin: 2vh 3vw 10vh 20vw;
background: rgb(11,35,238);
background: linear-gradient(90deg, rgba(11,35,238,1) 0%, rgba(246,52,12,1) 100%);
-webkit-text-fill-color: transparent;
-webkit-background-clip: text;
font-family: 'Anton';
font-size: 14vh;
text-transform: uppercase;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
h2 {
color: white;
margin: 0;
font-family: 'Raleway';
font-size: 10vh;
}
footer {
flex: 0 0;
min-height: 20vh;
display: flex;
align-items: center;
background: rgb(11,35,238);
background: linear-gradient(90deg, rgba(11,35,238,1) 0%, rgba(246,52,12,1) 100%);
padding: 2vh 3vw 2vh 3vw;
}
footer img {
width: auto;
height: 100%;
border-radius: 50%;
margin-right: 3vw;
}
Text-size calibration in the viewer reflects the actual resolution of the generated file, which is exactly what the open graph image will look like.

Adding a Feature Image
Posts won’t always have their own images, so the template needs an optional side image slot. Add a sideImage parameter to the manifest.
{
"partials": {
"content": "./content.html.mustache",
"styles": "./styles.css.mustache"
},
"parameters": [
{
"name": "title",
"type": "text",
"demoValue": "A picture is worth a thousand words"
},
{
"name": "sideImage",
"type": "imageUrl",
"demoValue": "https://resoc.io/assets/img/demo/photos/pexels-photo-371589.jpeg"
}
]
}
The HTML template uses a Mustache section to conditionally render that image slot.
<div class="wrapper">
<main>
{{#sideImage}}
<div class="sideImage"></div>
{{/sideImage}}
<h1>{{ title }}</h1>
</main>
<footer>
<img src="profil-pic.jpg" />
<h2>Philippe Bernard</h2>
</footer>
</div>
Styling the side image isn’t just a matter of an img tag; the CSS itself can accept the Mustache value for the background URL, keeping the rest of the layout intact.
{{#sideImage}}
.sideImage {
position: absolute;
width: 100%;
height: 100%;
background-image: url({{{ sideImage }}});
background-repeat: no-repeat;
background-size: auto 150vh;
background-position: -35vw 0vh;
-webkit-mask-image: linear-gradient(45deg, rgba(0,0,0,0.5), transparent 40%);
}
{{/sideImage}}
The template is finished. A report of the final look appears in the viewer.

The viewer also provides the exact command line to render an image for testing, producing output-image.jpg.

Deferring Generation with Plugins
Generating a social image for every page during the build adds significant time to each deploy. The image creation is instead deferred with two npm plugins: the Eleventy plugin and the framework-agnostic Netlify plugin.
npm install --save-dev @resoc/eleventy-plugin-social-image @resoc/netlify-plugin-social-image
Configure the Eleventy plugin in .eleventy.js. The templatesDir points at the template directory, and patchNetlifyToml tells the plugin to configure the Netlify plugin in netlify.toml on its own.
eleventyConfig.addPlugin(pluginResoc, {
templatesDir: 'resoc-templates',
patchNetlifyToml: true
});
In the base layout, a short code provides the social image URL for all pages. It takes the template name, a slug derived from the title, and a map of values from the manifest:
{% set socialImageUrl %}
{%- resoc
template = "default",
slug = (title or metadata.title) | slug,
values = {
title: title or metadata.title,
sideImage: featuredImage
}
-%}
{% endset %}
Then add the Open Graph tags inside the <head> of the layout. The previously declared variable works as-is for the og:image meta.
<meta property="og:title" content="{{ title or metadata.title }}"/>
<meta property="og:description" content="{{ description or metadata.description }}"/>
<meta property="og:image" content="{{ socialImageUrl }}"/>
<meta property="og:image:width" content="1200"/>
<meta property="og:image:height" content="630"/>
Applying the full featured-image version of the card requires editing a post so its front matter includes the optional featuredImage field.
---
title: This is my fourth post.
description: This is a post on My Blog about touchpoints and circling wagons.
date: 2018-09-30
tags: second tag
layout: layouts/post.njk
featuredImage: https://resoc.io/assets/img/demo/photos/pexels-pixabay-459653.jpg
---
After building, .gitignore has a new entry for resoc-image-data.json, which the Netlify plugin uses for lookup tables, and netlify.toml holds the plugin config that patchNetlifyToml inserted.
Deploying the site again brings up the social images. The homepage renders with the title-only template, while posts with a featuredImage show the full design.


Where the Time Goes
The automation primarily involves configuration—the real work was spent designing the template. That work happens in HTML and CSS with an integrated viewer, avoiding server setup and screenshot tooling. Much of the manual work that developers like Zach Leatherman had to do for their own setups is now handled by the plugin ecosystem around Resoc. The approach is generic enough that the Netlify companion plugin works for Next.js sites as well. A few lines of configuration give a blog unique, branded cards for every URL without taxing the build.



