Guide

Hugo Open Graph image

Hugo ships its own Open Graph and X card templates, but it prints them only where your layout calls them, and it prints og:image only when it can find an image for the page. The URL it prints is built from baseURL, so a wrong or local baseURL gives every card a picture no crawler can load.

Call the embedded templates

Hugo’s embedded templates docs give the call. Put both lines inside <head> in your base template or head partial:

{{ partial "opengraph.html" . }}
{{ partial "twitter_cards.html" . }}

That is the syntax since Hugo v0.146.0, which moved these templates into _partials. The new template system overview says to replace {{ template "_internal/opengraph.html" . }} with the partial call. Older Hugo versions need the old form. Current Hugo still accepts the old names: templates.go registers an _internal/ alias for each embedded partial “for a while”, so plan to switch rather than rely on it. Run hugo version to see which one you have. Everything below was checked against Hugo v0.169.0.

To change what the template prints, copy opengraph.html or twitter_cards.html into layouts/_partials/ under the same name. Your copy then wins over the embedded one.

Where og:image comes from

Both templates get their pictures from _funcs/get-page-images.html. In order:

  1. The page’s images front matter, a list. Every entry is used.
  2. If images is not set: a page resource (a file in the page bundle) whose name matches *feature*, or failing that *cover* or *thumbnail*. Only the first match is used.
  3. If there is still nothing: the first entry of params.images in the site configuration.
# content/blog/my-post/index.md
---
title: My post
description: One sentence people will see under the title.
images:
  - card.png
---

For each entry without a scheme, Hugo looks for a page resource with that name, then a global resource in assets/, and uses its permalink. If neither exists, it passes the path through absURL, which suits a file in static/. A full https:// URL is used as written. opengraph.html prints up to six og:image tags. twitter_cards.html uses the first image and sets twitter:card to summary_large_image; with no image at all it falls back to summary, the small card.

The rest comes from the page too: og:title is the page title, then the site title. og:description is the page description, then the summary, then params.description. og:type is article for regular pages and website for the home page and lists. og:url is the page’s .Permalink.

One default image for every page

Pages with no images and no feature, cover, or thumbnail file get no og:image unless the site sets a fallback:

# hugo.toml
baseURL = "https://www.example.com/"
title = "My site"

[params]
  description = "Text about my site"
  images = ["images/default-card.png"]   # static/images/default-card.png

A page’s own image still wins. 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.

baseURL must be your real https address

Crawlers need an absolute https:// image URL (absolute OG image URL covers why). Every URL Hugo prints here, from .Permalink or absURL, is built from baseURL. Set it to the full address people share, with the scheme and a trailing slash. A baseURL of / or a bare path gives you relative image URLs. Per the absURL docs, a path with a leading slash keeps only the protocol and host of baseURL, so on a site served from a subfolder write images/card.png, not /images/card.png.

The other trap is hugo server. In server.go, unless you pass --baseURL, the server swaps the host for localhost, adds the port, and turns https into http. The hugo server docs say it writes files to disk by default, and Hugo does not clear public/ between builds. If you upload public/ after a server session, cards point at http://localhost:1313. Deploy a fresh build instead: hugo --cleanDestinationDir (hugo build on recent versions). On a host that builds for you, check the build command does not pass a preview --baseURL to production.

Themes that print their own tags

Many themes already call these partials, or ship their own versions. PaperMod, for one, calls templates/opengraph.html and templates/twitter_cards.html from its head.html, and its opengraph.html uses cover.image from front matter first, then falls back to Hugo’s image lookup. So on PaperMod, set cover.image or images; adding Hugo’s partials again gives you two sets of tags. Before you add anything, search your theme’s layouts folder for og:image and read its docs for the front matter key it expects.

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 og:image means the page has no image Hugo could find and no params.images. No Open Graph tags at all means nothing in your layout calls the partials. A URL starting with / or http://localhost means fix baseURL and rebuild. Two sets of tags usually means a theme and your own partial call are both printing them; keep one.

Then open the image URL itself: it should return 200 with an image content type to an anonymous request. 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 Jekyll, GitHub Pages, or Eleventy instead? See Jekyll and GitHub Pages Open Graph image or Eleventy Open Graph image. Next.js and Astro are in Open Graph tags by framework.

FAQ

How do I add Open Graph tags in Hugo?

Call {{ partial "opengraph.html" . }} and {{ partial "twitter_cards.html" . }} in your head template. That is the form since Hugo v0.146.0; older versions use {{ template "_internal/opengraph.html" . }}.

Why does my Hugo page have no og:image?

Hugo found no image: no images front matter, no bundle file named like *feature*, *cover*, or *thumbnail*, and no params.images in the site config. Add params.images as a default.

Why does my Hugo og:image point at localhost?

The files came from hugo server, which swaps in a localhost address and writes to public/ by default. Set baseURL to your https address and deploy a clean hugo build.

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.