blog/src/posts/2024-10-26_opengraph-data.md
Sebin Nyshkim 7c73f7c1e0 build: 🔧 use externally hosted image service
Build time increases exponentially with more images pulled in by the eleventy-image plugin. This keeps build times low and shifts computational load to an external image hosting service.
2025-04-20 22:40:54 +02:00

16 KiB
Raw Blame History

title image tags
Open Graph Metadata and Images in Eleventy Made Easy
src alt credit width height type
https://img.sebin-nyshkim.net/i/28d944cf-3630-4db8-bc4e-469769c83d00 The Open Graph protocol logo sourrounded by the logos of Twitter, Mastodon, Telegram, Discord and the Fediverse Made with GIMP, logos © their respective owners 1200 630 image/png
eleventy

Blog posts are meant to be shared. When sharing links, you'll often see a preview on social media and instant messengers. But how does it work and how can you do it with Eleventy?

The metadata that makes share previews on social media and messengers work is called the Open Graph protocol. Originally conceived by Facebook to map relationships between people, it is now used primarily on social platforms to generate an eye-catching preview of a shared website that contains an image, a URL, a title and a short teaser of the content.

Open Graph metadata is basically just a bunch of <meta> tags in your site's <head> with attributes the social platforms recognize and know how to deal with. Now, you could write them all by hand, but that's tedious and pretty not fun. Luckily, there's plugins for Eleventy available that automate this and make it both easy to implement and highly customizable.

Picture-Perfect

The plugin that takes care of generating preview images is the Eleventy Open Graph image plugin. It's based on Vercel's Satori. It takes a file with a dedicated name to generate any kind of preview image from the HTML and CSS markup that you author.

By default, the Open Graph image plugin looks for a file with *.og.* in its file name. I'm using Nunjucks for my examples, because that's the template language I started out with and because it allows me to use variables to make the template more dynamic.

Then proceed to adding the plugin to Eleventy in its eleventy.config.js:

import fs from 'node:fs';
import EleventyPluginOgImage from 'eleventy-plugin-og-image';

export default async function (eleventyConfig) {
  eleventyConfig.addPlugin(EleventyPluginOgImage, {
    satoriOptions: {
      fonts: [
        {
          name: 'Inter',
          data: fs.readFileSync('../path/to/font-file/inter.woff'),
          weight: 700,
          style: 'normal',
        },
      ],
    },
    width: 3000,
    height: 2000
  });
}

The Open Graph image plugin allows you to define some options for the width and height of the preview image canvas (default: 1200 × 630). Additionally, you can pass Satori some font files in the satoriOptions object to make them available in the preview image. These need to be either TrueType (*.ttf) or Web Open Font Format (*.woff). WOFF2 is not supported! The fonts you can download from Google Fonts usually are TrueType, even the variable fonts. So go nuts!

Start by creating an og-image.og.njk file in the source directory of your Eleventy project (I'm using ./src/). In here you can build your preview with just HTML and CSS. However, Satori only supports a limited number of HTML elements and CSS properties. Refer to Satori's GitHub for a list of HTML elements it supports, as well as Yoga's documentation on the CSS properties that are supported.

Since the markup and styling is converted to SVG and then to images, it really doesn't matter if you use semantic HTML or not. Satori is perfectly happy to convert a bunch of <div> elements.

The Yoga layout engine is heavily based around Flexbox and it will likely shout at you if you forget to use display: flex on any of the elements in your markup. Make sure to keep an eye out for error messages in your terminal in case the preview stops updating. For example, Yoga does not like pseudo-elements and will stop updating previews if it detects them.

If you want a live preview of what Satori does to your preview and if it converts properly, you can do so by simply opening the preview HTML files it generates during live serving your Eleventy site. The Open Graph image plugin puts all the previews inside an og-images directory in your Eleventy output folder, e.g. if your output directory is located at ./public/ you'll find the previews in ./public/og-images/preview/. It mirrors the same file structure as your site. Whenever you update your og-image.og.njk template it will also update the preview in your browser with browsersync included in Eleventy. This way you can use the browser dev tools to adjust things to your liking and copy it back to your og-image.og.njk template (just like old days!), then save and update the preview to verify Satori converts the template correctly.

It's recommended to put your styles in a <style> block inside your og-image.og.njk for easier readability and better maintainability. You can use IDs or class names to apply styles. Some examples around Satori put it in the style attribute but this gets quite messy and hard to maintain after just a couple of CSS properties. Using HTML style attributes is best reserved for something that changes based on the content, like a background image linked in the blog post.

It might take some time to get used to how Satori needs you to write your HTML and CSS exactly, but it's not as bad as it might sound. My recommendation is to start with really simple, basic HTML and CSS, save often and go a little more old-school about the authoring process (especially making use of width and height CSS properties in more complex layouts so elements actualy span the whole area you're trying to fill).

Here's the template I use to generate my Open Graph images:

<style>
#top {
  display: flex;
  flex-flow: column nowrap;
  justify-content: flex-start;
  align-items: flex-start;
  position: relative;
  width: 100%;
  height: 100%;
  background-color: #0f172a;
  color: #e2e8f0;
  box-sizing: border-box;
}

#main {
  flex: 1 1 100%;
  display: flex;
  flex-flow: column nowrap;
  width: 100%;
  height: 100%;
}

#heading {
  flex: 1 1 100%;
  display: flex;
  flex-flow: column nowrap;
  justify-content: center;
  width: 100%;
}

#background {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

h1 {
  flex: 1 0 100%;
  display: flex;
  flex-flow: column nowrap;
  justify-content: center;
  padding: 64px 112px;
  font-size: 72px;
  text-wrap: balance;
  background-color: rgb(15 23 42 / 0.65);
}

#footer {
  flex: 1 0 96px;
  display: flex;
  flex-flow: row nowrap;
  align-items: center;
  gap: 24px;
  width: 100%;
  padding: 0 24px;
  background-color: rgb(8 47 73);
  font-size: 28px;
  font-weight: bold;
}

#footer #avatar {
  flex: 0 0 64px;
  display: flex;
  width: 64px;
  height: 64px;
  background-size: 100% 100%;
  border: 4px solid currentColor;
  border-radius: 100%;
}

#footer #meta {
  flex: 1 0 0;
  display: flex;
  flex-flow: row nowrap;
  justify-content: space-between;
}
</style>
{% raw %}
<div id="top">
  <div id="main">
    <div id="heading">
      {% if image and image.src != '' %}
        <img id="background" src="{{ image.src }}" alt="{{ image.alt }}">
      {% endif %}
      <h1>{{ title | safe }}</h1>
    </div>

    <div id="footer">
      <div id="avatar" style="{% if author and author.image != '' %} background-image: url({{ author.image }}){% endif %}"></div>
      <div id="meta">
        <p id="author">by {{ author.name }}</p>
        <p id="blog">blog.sebin-nyshkim.net</p>
      </div>
    </div>
  </div>
</div>
{% endraw %}

If you need to refer to some data inside your Open Graph image template, you have to explicitly pass it when using the ogImage shortcode in your base layout template, as the plugin does not pull these automatically from the data cascade! Stuff like page title, author, site address, image URLs, etc.

You first declare the name of the variable you want to make available in the Open Graph image template and the data you want to pass down to it:

<!DOCTYPE html>
<html lang="en">
  <head>
    {% raw %}{% ogImage "og-image.og.njk", { title: pageTitle, author: pageAuthor, image: heroImage } %}{% endraw %}
  </head>
  <body>
    <!-- site content here -->
  </body>
</html>

This makes the data of pageTitle, pageAuthor and heroImage available inside the template og-image.og.njk under the variable names title, author and image respectively.

Getting Meta

The Open Graph image is just one piece of the puzzle when it comes to making your previews stand out. Sites and apps processing your site also need a little more info to generate meaningful previews.

This is where another Eleventy plugin comes in, called Metagen. It allows you to add a bunch of <meta> tags to the <head> section of your layouts via the metagen shortcode. For a list of supported options refer to the Metagen docs.

The metagen shortcode takes values as either literals or dynamically, passed via Eleventy's data cascade. This is useful if you have data defined in data files or front matter data somewhere along the cascade and want every page to have Open Graph metadata specific to its content. This means you can have something like this in your base layout:

<!DOCTYPE html>
<html lang="en">
  <head>
    {% raw %}{% metagen
        title             = title + ' - Sebin\'s Blog',
        desc              = description,
        url               = 'https://blog.sebin-nyshkim.net' + page.url,
        type              = type,
        site_name         = 'Sebin\'s Blog',
        og_image_width    = image.width,
        og_image_height   = image.height,
        og_image_alt      = image.alt,
        og_image_type     = image.type,
        twitter_card_type = twitter.cardType,
        twitter_handle    = twitter.account,
        name              = author.name,
        generator         = 'eleventy',
        preconnect        = ['https://cdn.sebin-nyshkim.net'],
        dns_prefetch      = ['https://cdn.sebin-nyshkim.net'],
        css               = ['/fonts/tilt-warp/tilt-warp.css',
                             '/fonts/encode-sans/encode-sans.css',
                             '/fonts/m-plus-1-code/m-plus-1-code.css',
                             '/css/style.css',
                             '/css/prism.css']
    %}
    {% ogImage "og-image.og.njk", { title: title, author: author, image: image } %}{% endraw %}
  </head>
  <body>
    <!-- page content here -->
  </body>
</html>

Have a data file in a sub-directory somewhere:

{
  "layout": "blogpost.njk",
  "permalink": "/posts/{{ title | slugify }}/",
  "date": "git Created",
  "type": "article",
  "author": {
    "name": "Sebin Nyshkim",
    "href": "https://blog.sebin-nyshkim.net",
    "image": "https://blog.sebin-nyshkim.net/img/sebin.png"
  },
  "twitter": {
    "cardType": "summary_large_image",
    "account": "SebinNyshkim"
  }
}

And have front matter that looks like this:

---
title: Responsive, Self-hosted Images for Your Eleventy Blog
description: While you can certainly host your image files with the Git repo your Eleventy site is checked into, or add them manually after building it, neither option is ideal if you want responsive images in multiple formats to save precious bandwidth.
image:
  src: https://cdn.sebin-nyshkim.net/-iTHSLFBdpY
  alt: Close-up of SVG code on a computer screen
  width: 1200
  height: 630
  type: 'image/png'
tags: ["self-hosting", "docker", "eleventy"]
---

## Getting visual

Images make articles a lot prettier to look at, don't you think?

This results in output that looks like this:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Responsive, Self-hosted Images for Your Eleventy Blog - Sebin's Blog</title>
    <meta name="title" content="Responsive, Self-hosted Images for Your Eleventy Blog - Sebin's Blog">
    <link rel="preconnect" href="https://cdn.sebin-nyshkim.net">
    <link rel="dns-prefetch" href="https://cdn.sebin-nyshkim.net">
    <meta name="author" content="Sebin Nyshkim">
    <meta name="description" content="While you can certainly host your image files with the Git repo your Eleventy site is checked into, or add them manually after building it, neither option is ideal if you want responsive images in multiple formats to save precious bandwidth.">
    <meta name="generator" content="eleventy">
    <meta property="og:type" content="article">
    <meta property="og:url" content="https://blog.sebin-nyshkim.net/posts/responsive-self-hosted-images-for-your-eleventy-blog/">
    <meta property="og:site_name" content="Sebin's Blog">
    <meta property="og:locale" content="en_US">
    <meta property="og:title" content="Responsive, Self-hosted Images for Your Eleventy Blog - Sebin's Blog">
    <meta property="og:description" content="While you can certainly host your image files with the Git repo your Eleventy site is checked into, or add them manually after building it, neither option is ideal if you want responsive images in multiple formats to save precious bandwidth.">
    <meta property="og:image:alt" content="Close-up of SVG code on a computer screen">
    <meta property="og:image:width" content="1200">
    <meta property="og:image:height" content="630">
    <meta property="og:image:type" content="image/png">
    <meta name="twitter:card" content="summary_large_image">
    <meta name="twitter:site" content="@SebinNyshkim">
    <meta name="twitter:creator" content="@SebinNyshkim">
    <meta name="twitter:url" content="https://blog.sebin-nyshkim.net/posts/responsive-self-hosted-images-for-your-eleventy-blog/">
    <meta name="twitter:title" content="Responsive, Self-hosted Images for Your Eleventy Blog - Sebin's Blog">
    <meta name="twitter:description" content="While you can certainly host your image files with the Git repo your Eleventy site is checked into, or add them manually after building it, neither option is ideal if you want responsive images in multiple formats to save precious bandwidth.">
    <link rel="canonical" href="https://blog.sebin-nyshkim.net/posts/responsive-self-hosted-images-for-your-eleventy-blog/">
    <link rel="stylesheet" href="/fonts/tilt-warp/tilt-warp.css">
    <link rel="stylesheet" href="/fonts/encode-sans/encode-sans.css">
    <link rel="stylesheet" href="/fonts/m-plus-1-code/m-plus-1-code.css">
    <link rel="stylesheet" href="/css/style.css">
    <link rel="stylesheet" href="/css/prism.css">
  </head>
  <body>
    <!-- page content here -->
  </body>
</html>

Metagen will take care of filling in as much relevant metadata as you provide it with. Setting a title, url or desc option in Metagen will set the generic <meta> tags, as well as all the relevant og: and twitter: tags. If you set the more specific og_* or twitter_* Metagen options, they will take priority over the more general ones.

If any of the values passed to the shortcode options come up empty, i.e. there is no image.alt, image.width, image.height or image.type metadata anywhere along the data cascade, the tags will be left out.

You do not need to set the og_image or twitter_image options in Metagen. These will be provided by the Open Graph image generator plugin already.

Gettin' the Look

If you want to verify that all went according to plan, there a few ways to achieve that.

I like to use a little tool on Linux called Share Preview by Rafael Mardojai CM. It will mimic how your site's preview will look like when posted on social media. It even works with localhost URLs! It's available from Flathub.

The other method is using one of the couple dozen websites that will give you the same functionality, and maybe provide some insights and tips on how to optimize them for maximum sharability (if that's your thing).

And that's about it! Now go and make your site previews awesome! 😎