Γ—
Home RSS feed info
Set the content of Open Graph meta tags yourself.

Hugo - customized Open Graph integration

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 }} &middot; {{ .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 }} &middot; {{ .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:

Mastodon toot link-box
Mastodon Toot with Open Graph Link Box.

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:

Open Graph blog post extension
Firefox Browser Extension - Display for this blog post.
Open Graph website extension
Firefox Browser Extension - Display for all other websites except blog posts.

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

This might also interest you

Update:  |
12 minutes to read
0
This post was created with Hugo version 0.115.2.

With the German language setting, comments are not displayed in the English version of the website and vice versa.

© 2023 - Frank Kunert  -  About me