Hugo - customized Open Graph integration

Open Graph is a protocol that can be used to influence how a web page is displayed when it is shared on social media. Via meta entries in the head of each web page you can influence which data is used for this. In this article I describe my Open Graph meta tags, which are summarized in a partial.
General information about Open Graph
Originally, Open Graph was developed by Facebook. However, it has since been adopted by other social media channels such as Mastodon, Twitter, Pinterest, and LinkedIn.
Most content is shared as a URL on Facebook and other (Open Graph Protocol) OGP-supporting websites. For this reason, it can only be positive if one’s website is equipped with Open Graph meta tags. This is the only way to have control over what content is displayed there.
Since I want to understand what leads to a corresponding output in the source code and what possibilities there are, I looked at the following websites, among others:
Hugo Open Graph Template
Hugo provides a template for the integration of the Open Graph meta tags. The template is called in the head
HTML tag and integrates the meta tags. To use the template, the following call in the baseof.html is sufficient:
{{ template "_internal/opengraph.html" . }}
For a better understanding of what the template does, there is a Hugo doc and a link to the template source code:
Post updated on June 6, 2023
It wasn’t until a note in the - ahrefs Webmastertools - I noticed that for paginated web pages, i.e. my home page and my tag teaser list, I’m not passing a /page/X/ URL extension to my ogData.html partial.
This is more complicated than it seems at first glance. The GO programming language used by Hugo is designed to effectively protect memory areas. Put simply, the contents of a variable can be invalidated in a subsequent query or pass. I grew up with the C programming language π C pointers are wonderful for shooting yourself in the foot. That’s why to this day it’s sometimes incomprehensible to me when I run into a GO wall.
Well, I think Hugo is good and someday I will understand the peculiarities of GO. Long story short, the partial must be passed the page context and the complete URL of the current web page. Sometimes the URL contains a normal web page and sometimes a paginated web page with the extension /page/X/. The partial doesn’t care, it just outputs the URL.
Additionally, I have adapted the multilingual entries in hugo.toml
to the changes in Hugo Version 0.112.
My Partial ogData.html
The Hugo template is designed for many purposes. Since I only want to output the content adapted for my website, I have taken over or rewritten parts of the template. More about this below.
I have included the partial ogData.html in the head
tag under the source code for - SEO - Self-referencing hreflang on multilingual website
- included in my baseof.html
. So I can use the variable $href
filled there to pass it to the partial:
{{ partial "ogData" (dict "context" . "href" $href) }}
The Hugo documentation - Partial Templates - says nothing about passing multiple parameters to a partial. Mert Bakir has a helpful post on this in his interesting blog - How To Pass Arguments in Hugo Partials .
The source code of the partial themes/tekki/layouts/partials/ogData.html
looks like this:
<meta property="og:title" content="{{ if .context.IsHome }}{{ .context.Site.Params.mydomain }}{{ else }}{{ .context.Title }} · {{ .context.Site.Params.mydomain }}{{ end }}">
<meta property="og:type" content="{{ if .context.IsPage }}article{{ else }}website{{ end }}">
<meta property="og:description" content="{{ if .context.Description }}{{ .context.Description }}{{ else }}{{ .context.Site.Params.description }}{{ end }}">
<meta property="og:site_name" content="{{ .context.Site.Params.mydomain }}">
<meta property="og:url" content="{{ .href }}">
{{ if eq .context.Site.Language.Lang "de" }}
<meta property="og:locale" content="de_DE">
<meta property="og:locale:alternate" content="en_GB">
{{ else }}
<meta property="og:locale" content="en_GB">
<meta property="og:locale:alternate" content="de_DE">
{{ end }}
{{ if and (.context.IsPage) (ne .context.Section "") }}<meta property="article:section" content="{{ .context.Section }}">{{ end }}
{{ $iso8601 := "2006-01-02T15:04:05-07:00" }}
{{ with .context.PublishDate }}<meta property="article:published_time" {{ .Format $iso8601 | printf "content=%q" | safeHTMLAttr }}>{{ end }}
{{ with .context.Lastmod }}<meta property="article:modified_time" {{ .Format $iso8601 | printf "content=%q" | safeHTMLAttr }}>{{ end }}
{{ $image := .context.Resources.GetMatch "featured" }}
{{ with $image }}
<meta property="og:image" content="{{ .Permalink }}">
<meta property="og:image:type" content="{{ .MediaType }}">
<meta property="og:image:width" content="{{ .Width }}">
<meta property="og:image:height" content="{{ .Height }}">
<meta property="og:image:alt" content="{{ .Title }}">
<meta property="og:image:secure_url" content="{{ .Permalink }}">
{{ else }}
<meta property="og:image" content="https://tekki-tipps.de/tekki-tipps-og.png">
<meta property="og:image:type" content="image/png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
{{ if eq .context.Site.Language.Lang "de" }}
<meta property="og:image:alt" content="Blog ΓΌber Hugo, Webdesign, CSS/SCSS, SEO, Tools">
{{ else }}
<meta property="og:image:alt" content="Blog about Hugo, web design, CSS/SCSS, SEO, Tools">
{{ end }}
<meta property="og:image:secure_url" content="https://tekki-tipps.de/tekki-tipps-og.png">
{{ end }}
For better understanding I will describe each meta tag individually.
meta property=“og:title”
<meta property="og:title" content="{{ if .context.IsHome }}{{ .context.Site.Params.mydomain }}{{ else }}{{ .context.Title }} · {{ .context.Site.Params.mydomain }}{{ end }}">
If the current web page is the front page, the title
is taken from the hugo.toml
. Otherwise, for all other web pages, the title
is taken from the Front Matter entry of the current web page. A hyphen is inserted and the title
from the hugo.toml
is appended.
The context of the page is passed in context
. Therefore, queries and content must be extended with .context
.
The entries of the hugo.toml
for German and English look like this:
..
[languages]
[languages.de]
languageName = "Deutsch"
weight = 1
title = "Hugo, Webdev, SEO, Tools"
[languages.de.params]
description = "Blog ΓΌber Hugo, Webdev, SEO, Tools"
mydomain = "tekki-tipps.de π©πͺ"
..
[languages.en]
languageName = "English"
weight = 2
title = "Hugo, Webdev, SEO, Tools"
[languages.en.params]
description = "Blog about Hugo, Webdev, SEO, Tools"
mydomain = "tekki-tipps.de/en/ π¬π§"
..
The entries in the hugo.toml
were subsequently adapted by me to the Hugo version 0.112. See also - Multilingual - Changes in Hugo as of version 0.112
.
meta property=“og:type”
<meta property="og:type" content="{{ if .context.IsPage }}article{{ else }}website{{ end }}">
On my website the og:type website
is displayed for the home page, the tag cloud and on tag lists. So on all overview lists. All other web pages get the og:type article
.
meta property=“og:description”
<meta property="og:description" content="{{ if .context.Description }}{{ .context.Description }}{{ else }}{{ .context.Site.Params.description }}{{ end }}">
If the current web page in Front Matter has a description
, this is taken over by .Description
. Otherwise the description
of the hugo.toml
is used. <update Apr 8, 2023> If the description is longer than 120 characters, the Firefox browser mangles the og:description
in version 111.0.1, but it also does this in the meta
Description tag. Google Chrome, Safari and Edge do not have this problem. The ogp.me website does not give an exact length. So this is a Firefox error. </update>
meta property=“og:site_name”
<meta property="og:site_name" content="{{ .context.Site.Params.mydomain }}">
The title from the hugo.toml
.
meta property=“og:url”
<meta property="og:url" content="{{ .href }}">
The URL of the current web page, including any extension /page/X/. This results in the total change effort.
meta property=“og:locale” und “og:locale:alternate”
{{ if eq .context.Site.Language.Lang "de" }}
<meta property="og:locale" content="de_DE">
<meta property="og:locale:alternate" content="en_GB">
{{ else }}
<meta property="og:locale" content="en_GB">
<meta property="og:locale:alternate" content="de_DE">
{{ end }}
og:locale
defines the language of the current web page. og:locale:alternate
tells that there is a translation in an alternate language for the current web page. Since I only cover 2 languages, an if/else is enough for me.
meta property=“article:section”
{{ if and (.context.IsPage) (ne .context.Section "") }}<meta property="article:section" content="{{ .context.Section }}">{{ end }}
If the current web page is a Page and the .Section
is not an empty string, then the meta tag should be written in the Head. On my website there is only the section blog
. To prevent the meta tag from being written to the head for web pages that do not have a section, a query is made to see if the string in .Section
is empty.
meta property=“article:published_time” and article:modified_time
{{ $iso8601 := "2006-01-02T15:04:05-07:00" }}
{{ with .context.PublishDate }}<meta property="article:published_time" {{ .Format $iso8601 | printf "content=%q" | safeHTMLAttr }}>{{ end }}
{{ with .context.Lastmod }}<meta property="article:modified_time" {{ .Format $iso8601 | printf "content=%q" | safeHTMLAttr }}>{{ end }}
The overview pages - home page, tag cloud and tag lists - do not have .PublishDate
hence the query. All other web pages may not have .Lastmod
yet.
meta property=“og:image”
{{ $image := .context.Resources.GetMatch "featured" }}
{{ with $image }}
<meta property="og:image" content="{{ .Permalink }}">
<meta property="og:image:type" content="{{ .MediaType }}">
<meta property="og:image:width" content="{{ .Width }}">
<meta property="og:image:height" content="{{ .Height }}">
<meta property="og:image:alt" content="{{ .Title }}">
<meta property="og:image:secure_url" content="{{ .Permalink }}">
{{ else }}
<meta property="og:image" content="https://tekki-tipps.de/tekki-tipps-og.png">
<meta property="og:image:type" content="image/png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
{{ if eq .context.Site.Language.Lang "de" }}
<meta property="og:image:alt" content="Blog ΓΌber Hugo, Webdesign, CSS/SCSS, SEO, Tools">
{{ else }}
<meta property="og:image:alt" content="Blog about Hugo, web design, CSS/SCSS, SEO, Tools">
{{ end }}
<meta property="og:image:secure_url" content="https://tekki-tipps.de/tekki-tipps-og.png">
{{ end }}
Now it gets a little more complicated. Therefore I have to explain a bit more. My web pages are multilingual in two languages. I use PageBundles because of the clarity - everything for one web page, in one directory. The respective images are stored inside the PageBundle directory in the img
directory. My blog post pages have an article image and an image designated as featured
. All other web pages do not have an image designated as featured
.
In Front Matter for this web page, the entries look like this:
resources:
- name: featured
src: img/featured.png
title: Customized Open Graph integration
- name: article-img
src: img/open-graph.png
title: Customized Open Graph integration
Using .context.Resources.GetMatch
, I fetch the image designated as featured
from the PageBundle and store it in the $image
variable. If a featured
image is present, I output the appropriate meta tags for that image.
If there is no featured
image, I give the direct URL for a “substitute” image. I have stored this image in the static
directory at the top level. The “replacement image” is used for web pages outside my blog post pages.
The images should have a resolution of 1200px X 600px to display a good image even on high resolution screens. See also the Facebook link above.
The open source social network Mastodon uses the Open Graph meta
tags for the automatic creation of a link box. For this, of course, corresponding Open Graph meta
tags must be present on the target page. For the first URL specified in a toot, a thumbnail of the og:image
is created in this link box. The og:title
, og:site_name
and the og:url
are also used. The whole thing will look like this:

When inserting an image in a Mastodon Toot, the link box is not created. This totally irritated me at first because I thought I was doing something wrong. The Mastodon documentation doesn’t mention this with a word.
meta property=“og:image:type”
I use image files of type png
and jpg
. png
are smaller in size if a large part of the image contains the same color components. The type is automatically determined by .MediaType
.
meta property=“og:image:width”
With .Width
the width of the image is determined.
meta property=“og:image:height”
With .Height
the height of the image is determined.
meta property=“og:image:alt”
For the blog posts, I take the alternate text of the image from the Resource Title. For all other web pages, with the general image, I save my alt text manually. Again, the query for the language and since I only use 2 languages an if/else construct.
meta property=“og:image:secure_url”
Nowadays there are hardly any websites that do not have an SSL certificate. The meta tag is from another time. But should be specified. Also here I pass the .Permalink
or the direct URL of the “substitute image”.
Checking the Open Graph parameters
In the head
of the HTML source code you can look at the corresponding meta tags. Since I created the Open Graph meta tags after the fact, it is very time-consuming to check this for each post and language.
There is a solution for this as well - browser extensions. For Safari I haven’t found an extension at all. For Edge there is a text solution, but the relevant part has to be scrolled. Then you can also look at the HTML source code.
For Firefox and Chrome there is a corresponding extension. The one for Firefox I find nicer and use it too:


Conclusion
I do not have a Facebook account and therefore cannot control the sharing of links within Facebook. Sharing my contact addresses is too valuable to me to be forced to share with Facebook. Others see this differently and for those people I have integrated the OG meta tags as well. Recently I am on - Mastodon - and use the Open Graph meta tags there myself.
Link list for this article
- The Open Graph protocol
- Facebook - Images in shared links
- Hugo Documentation - Open Graph
- Hugo Template opengraph.html
- ahrefs.com - free webmaster tools
- SEO - Self-referencing hreflang on multilingual website
- Hugo Documentation - Partial Templates
- Mert Bakir - How To Pass Arguments in Hugo Partials
- Firefox - Open Graph Preview
- Google Chrome - OGraph Previewer
This might also interest you
- Hugo external links - nofollow or not nofollow - equal to dofollow?
Why I no longer use - rel=nofollow - for external links.
- Hugo - Structured schema data for better SEO
Schema.org type WebSite and BlogPosting - structured data for better SEO of my Hugo website.
- Hugo - Tutorial Part 1 - Create i18n multilingual site
i18n - Part 1 of 3 sequential tutorials for creating a Hugo multilingual website.
With the German language setting, comments are not displayed in the English version of the website and vice versa.