OG images in Hugo
Hugo builds a static site — there's no server or build step that can compute an HMAC
signature, so the signed GET /i/{keyId}/{sig}
route (Signing) isn't reachable from a template. Use a
public key instead: it accepts unsigned requests for the
templates you explicitly allow, which is all a Hugo partial needs.
The blog template is a good starting card.
1. Create a public key
Sign in at /login and open the dashboard. Click New key,
choose Public key, select which templates the key may render (e.g.
blog), then Create key. The key id (starts with
k_) is safe to commit into your site's source — it's not the bearer
secret.
Still on a hand-issued beta key? Email support@ogmake.com.
2. Add it to the head partial
In layouts/partials/opengraph.html (included from
baseof.html's <head>):
<meta property="og:image" content="https://ogmake.com/p/{your-public-key}/blog?site=example.com/blog&author={{ .Params.author | urlquery }}&date={{ .Date.Format "Jan 2, 2006" | urlquery }}&title={{ .Title | plainify | urlquery }}" /> Include it once from your base template so every page — post, section, taxonomy — gets a tag built from its own front matter:
<!-- layouts/_default/baseof.html -->
<head>
...
{{ partial "opengraph.html" . }}
</head> Gotcha: an un-urlquery'd title breaks the URL, not just the image
.Title and .Params.* often contain
spaces, &, or quotes straight from front matter — pass every
value through Hugo's urlquery function before it lands in the
query string, or an ampersand in a title silently truncates the params after it (the rest of
the string gets parsed as a new, wrong query key) rather than producing a visible error.
.Title can also carry Markdown/HTML from a shortcode-heavy title
— pipe it through plainify first so the API template (which
renders plain text, not markup) doesn't get raw tags in its title field.
Versus generating images in the build
Hugo can draw text onto images itself with its image filters (images.Text),
or you can run a script over your content before the build. Both are free and keep
everything in your repo. The trade-off is layout: text filters give you a background image
with one line of text, and a script means adding Node or a headless browser to your CI. The
partial above needs neither, at the cost of an external service and the
plan limits (Free is 100 renders a month; paid from $12 for 2,000).
Images are rendered the first time a crawler asks for them, then cached.
Verify it
hugo server runs on localhost, which
social crawlers can't reach — check the tag renders correctly locally (curl -s http://localhost:1313/posts/some-post/ | grep 'og:image'), but do the real preview check against the deployed URL. Because the image URL is built
from front matter, an unsigned request with a bad or missing field still returns a
200 fallback image rather than an error page — inspect the
response header instead: curl -I "https://ogmake.com/p/..."
and look for x-ogmake-error (see
debugging a public URL). Once the tag is right, run
the live page through Facebook's
Sharing Debugger, or compose a new post on X with the URL and check the card renders
there (X retired its public Card Validator in 2022) — since a Hugo rebuild that only changes
template design (not front matter)
produces the exact same image URL, add a v query param tied to
your build (e.g. Hugo's hugo.Version or a deploy timestamp) so a
pure design change still gets a fresh crawl instead of a stale cached preview. The
OG image checker reads a deployed page's tags and previews the card.