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

<!-- more -->

The metadata that makes share previews on social media and messengers work is called the [Open Graph protocol](https://ogp.me/). 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](https://www.npmjs.com/package/eleventy-plugin-og-image). It's based on Vercel's [Satori](https://github.com/vercel/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`:

```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](https://fonts.google.com/) 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](https://github.com/vercel/satori#html-elements) for a list of HTML elements it supports, as well as [Yoga's documentation](https://www.yogalayout.dev/) 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](https://css-tricks.com/snippets/css/a-guide-to-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:

```twig
<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:

```twig
<!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](https://www.npmjs.com/package/eleventy-plugin-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](https://metagendocs.netlify.app/docs/intro/).

The `metagen` shortcode takes values as either literals or dynamically, passed via Eleventy's [data cascade](https://www.11ty.dev/docs/data-cascade/). This is useful if you have data defined in [data files](https://www.11ty.dev/docs/data-template-dir/) or [front matter](https://www.11ty.dev/docs/data-frontmatter/) 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:

```twig
<!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:

```json
{
  "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:

```md
---
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:

```html
<!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](https://apps.gnome.org/SharePreview/) by [Rafael Mardojai CM](https://mardojai.com/). 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](https://flathub.org/apps/com.rafaelmardojai.SharePreview).

The other method is using one of the [couple dozen websites](https://duckduckgo.com/?q=sharing+preview+tester) 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! 😎
