Guide
Eleventy (11ty) Open Graph image
Eleventy has no built-in Open Graph or SEO tags. It writes exactly the HTML your layout gives it, so a page has an og:image only if your base layout prints one, and the URL is absolute only if you build it from your domain. Eleventy does not know your domain until you put it in your data.
Put the tags in your base layout
Keep the site-wide values in a global data file. Anything in _data/ is available to every template under the file’s name:
// _data/site.json
{
"url": "https://www.example.com/",
"title": "My site",
"description": "Text about my site",
"image": "/img/default-card.png"
}
Then print the tags inside <head> in the layout every page uses, here _includes/base.njk in Nunjucks. Each tag takes the page’s own value and falls back to the site default:
{%- set ogImage = image or site.image %}
<meta property="og:title" content="{{ title or site.title }}">
<meta property="og:description" content="{{ description or site.description }}">
<meta property="og:type" content="{{ 'website' if page.url == '/' else 'article' }}">
<meta property="og:url" content="{{ page.url | htmlBaseUrl(site.url) }}">
<meta property="og:image" content="{{ ogImage | htmlBaseUrl(site.url) }}">
<meta name="twitter:card" content="summary_large_image">
A post sets its own values in front matter:
---
layout: base.njk
title: My post
description: One sentence people will see under the title.
image: /img/my-post-card.png
---
page.url is the page’s output path, such as /blog/my-post/, from Eleventy supplied data. Because these are ordinary data cascade values, you can also set image once for a whole folder in a directory data file (for example blog/blog.json); a page’s front matter still wins over it. Keep the site.image default: in our test, htmlBaseUrl stopped the build with an error on a page where the value was undefined.
Make the URLs absolute with htmlBaseUrl
Crawlers need a full https:// image URL (absolute OG image URL covers why). The htmlBaseUrl filter comes from the HTML <base> plugin, which ships with Eleventy but has to be added in your config. Without it, Eleventy stops with filter not found: htmlBaseUrl:
// eleventy.config.js
import { HtmlBasePlugin } from "@11ty/eleventy";
export default function (eleventyConfig) {
eleventyConfig.addPlugin(HtmlBasePlugin);
eleventyConfig.addPassthroughCopy("img");
}
Passed a full URL as its argument, the filter returns an absolute URL and adds your pathPrefix if you deploy to a subfolder: /img/default-card.png becomes https://www.example.com/img/default-card.png. A URL that is already absolute is left alone. In Liquid the call is {{ page.url | htmlBaseUrl: site.url }}. The source is HtmlBasePlugin.js.
Two things to know. First, the plugin also rewrites URLs in elements such as a[href] and img[src] on its own, but not meta tags: in our build a hand-written <meta property="og:image" content="/img/card.png"> came out still relative. Use the filter in the tag. Second, write image paths starting with /. A relative value such as card.png is resolved against the page’s URL, so on /blog/my-post/ it becomes /blog/my-post/card.png, which only works if the file is copied there. The RSS plugin’s older absoluteUrl filter is marked deprecated in the RSS plugin docs in favor of the HTML <base> plugin.
Everything here was checked by building a test site with Eleventy v3.1.7, the current release, and Eleventy Image v7.1.1.
Copy the image into the output
Eleventy only processes template files. An image in your project does not reach _site/ unless you tell Eleventy to copy it with passthrough file copy. If it is missing, the tag is right but the image URL returns a 404:
eleventyConfig.addPassthroughCopy("img"); // img/ → _site/img/
eleventyConfig.addPassthroughCopy("src/img"); // with input "src": → _site/img/
eleventyConfig.addPassthroughCopy("blog/**/*.png"); // images next to posts
Paths are relative to the project root, not the input directory, and the input directory is stripped from the output path. Eleventy v3.1 also has an html-relative copy mode, but it copies only files referenced from elements such as img, video, and a. In our test it did not copy an image used only in an og:image meta tag, so use a plain passthrough copy for share images.
Generate the card with Eleventy Image (optional)
If you want Eleventy to resize the share image at build time, @11ty/eleventy-img can do it through its JavaScript API. Image v7 requires Node 22 or newer. A shortcode that returns the absolute URL of a 1200px-wide JPEG:
// eleventy.config.js
import Image from "@11ty/eleventy-img";
export default function (eleventyConfig) {
eleventyConfig.addShortcode("ogImageUrl", async function (src) {
const metadata = await Image(src, {
widths: [1200],
formats: ["jpeg"],
outputDir: "./_site/img/og/",
urlPath: "https://www.example.com/img/og/",
});
return metadata.jpeg[0].url;
});
}
<meta property="og:image" content="{% ogImageUrl "./img/hero.png" %}">
A full URL in urlPath is supported since Image v6.0.0, so the result is already absolute. Eleventy Image keeps the source’s aspect ratio and does not upscale raster images, so it will not crop to 1200×630 for you. Start from an image at that ratio, 1200 pixels wide or larger. From a 2400×1260 PNG, the shortcode above wrote a 1200×630 JPEG. The Image HTML Transform, the plugin’s default setup, only processes <img> and <picture> elements, not meta tags.
Size and card type
Make the default a 1200×630 PNG or JPEG: OG image size and OG image format say why, and the Open Graph image resizer crops one in the browser. Set twitter:card to summary_large_image for the large landscape card on X. Twitter Card types has the details. The tags themselves are defined by the Open Graph protocol.
The dev server is not the live site
npx @11ty/eleventy --serve serves your site at http://localhost:8080/ (the dev server’s default port) and writes the pages to _site/. Your tags still use site.url, so a local page points at the production image, which does not exist yet if you just added it. Preview bots cannot reach localhost either. Test cards against the deployed page.
If you switch site.url to a localhost address during development, for example in a _data/site.js that reads process.env.ELEVENTY_RUN_MODE (one of build, serve, or watch, per the environment variables docs), deploy the output of a plain npx @11ty/eleventy build, not a _site/ folder left over from --serve. Otherwise every card points at http://localhost:8080.
Check the live page, then re-scrape
Read what the server sends to a preview bot, not your local preview:
curl -sL -A "facebookexternalhit/1.1" https://www.example.com/blog/my-post/ \
| grep -iE 'og:image|og:url|twitter:card'
You want one og:image with an absolute https:// URL on your real domain, and a twitter:card of summary_large_image. No Open Graph tags at all means the page does not use the layout that prints them, or a layout key is missing. A URL starting with / means the tag skipped htmlBaseUrl. A URL on localhost means a development build was deployed. Two og:image tags usually means a starter template or plugin prints them too; keep one.
Then open the image URL itself: it should return 200 with an image content type to an anonymous request. A 404 usually means a missing passthrough copy. If the tags are right and an app still shows the old card, it cached the earlier fetch. Clear Open Graph cache lists the debugger or workaround for each app, and og:image not showing walks the other causes. The free partial check on the check page names whether og:image and og:title are set, and the site audit finds every page missing og:image.
On Hugo or Jekyll instead? See Hugo Open Graph image and Jekyll and GitHub Pages Open Graph image. Next.js and Astro are in Open Graph tags by framework.
FAQ
How do I add Open Graph tags in Eleventy?
Write them yourself in the <head> of your base layout. Eleventy has no built-in SEO tags. Read title, description, and image from the page, with defaults from _data/site.json.
How do I make an absolute og:image URL in Eleventy?
Add HtmlBasePlugin and write {{ image | htmlBaseUrl(site.url) }}, with site.url set to your https address. The plugin’s automatic rewrite skips meta tags, so the filter is needed.
Why is my Eleventy og:image a 404?
The file was never copied to _site/. Add its folder with eleventyConfig.addPassthroughCopy. The html-relative mode does not copy files referenced only from a meta tag.
What does a CardScope check cost?
Previews for X, LinkedIn, Slack, Discord, iMessage, Bluesky, Threads, Mastodon, WhatsApp, Telegram, and Microsoft Teams, plus the fix checklist, are Pro: $19 once via Stripe Checkout. No account. A sample report shows the shape of the result before you pay. A free partial check names whether og:image and og:title are set. We parse HTML in memory and discard it — we do not store the page.