Creating Labarum (Part 5): Schema and Open Graph

Websites are read by more than just human eyes. Browsers, search engines, screen readers, and more take the information being served to them about a site and use it for different purposes. I wanted to make sure that my site could be parsed properly so that it can be reached to everyone who wants it. The web has several technologies designed for this purpose that do not add too much more work to incorporate them. I focused on Schema and OpenGraph meta1 tags in the <head> element to achieve this. An excellent resource for learning about it can be found at HTML Head.


Hugo offers ways of doing this by having your template reference some internal templates. For example, the very popular template Ananke uses the following code to utilize these templates.

{{- template "_internal/opengraph.html" . -}}
{{- template "_internal/schema.html" . -}}
{{- template "_internal/twitter_cards.html" . -}}

At the beginning of this process, I found that including this would cause the site not to render on That issue has since been resolved, but I kept with my code for my understanding and further customization.

The good thing about all of this is that Ananke has a link directly over the templates so that you can review the code yourself.

It can be found here: Embedded Templates

Another key difference is that the templates also have logic for a featured image. In most of my usage, I don’t have images and don’t know how to specify an image as featured in markdown. I’ll have to reevaluate this at a later date.

Heading Home

When creating a template for blog, you should be aware that not all tags that you would have for your articles apply to your main page.

I used a flag that Hugo provides to create a section in the header for the home page to keep tags that a specific t the landing page.

{{- if .IsHome -}}

For the individual articles, I’ll be checking the title property in to determine logic on what tags that will be used.

{{ if isset .Params "title" }}

When you read the article, you will see different times that I specified something and may think that I could have been more clever by writing it in a way where I don’t have to repeat myself. I did this for a few reasons.

  1. I’m learning how Hugo works with templates
  2. I find that clever code can be difficult to debug and understand (You can thank me later, future me)

Getting a head of the page

I wanted to start off with the <head> elements that uses before checking to see if there were other things that I needed to add for the website.

I like to look at the Marfa’s head partial because it’s the theme that Manton uses.

{{ "<!-- Values -->" | safeHTML }}
<link rel="shortcut icon" 
	href="{{ .Site.Author.username }}/favicon.png"
	type="image/x-icon" />
<link rel="me" href="{{ .Site.Author.username }}" />
<link rel="authorization_endpoint" href="" />
<link rel="token_endpoint" href="" />
<link rel="micropub" href="" />
<link rel="microsub" href="" />
<link rel="webmention" href="" />
<link rel="subscribe" href="" />
<link rel="canonical" href="{{ .Permalink }}" />	

A lot of the these values are specific to how and the open web respond to the site in general. Blogging software such as MarsEdit, will use this information to connect to your blog for editing. Webmentions are a way for different systems to communicate to regarding if the source content has been referenced somewhere else.

{{ if .RSSLink -}}
<link href="{{ .RSSLink }}" rel="alternate" type="application/rss+xml" title="{{ .Site.Title }}" />
      <link href="{{ "podcast.xml" | absURL }}" rel="alternate" type="application/rss+xml" title="Podcast" />
      <link rel="alternate" type="application/json" title="{{ .Site.Title }}" href="{{ "feed.json" | absURL }}" />
      <link rel="EditURI" type="application/rsd+xml" href="{{ "rsd.xml" | absURL }}" />
    {{ end -}}

These allow for newsreaders and podcast aggregators to get the information needed to get information about updates. One cool thing is the use of a JSON feed that Manton helped build.

I also found a website dedicated to explaining a lot of the different tags that can be found in the <head> element.

Open examples

The first set of tags that I looked at were ones related to OpenGraph tags. These where the ones that a supported in social media (facebook, twitter, slack, etc.) and operating systems that I was using at the beginning of the project.

OpenGraph tags behave the same way as <title> and other tags in a web page. In fact, for some services, if an OpenGraph tag is not there, it will default to other tags in the head. For example, title or description. I decided to define the values for the tag to make sure that the site is compliant2.

The key difference between OpenGraph tags and the standard tags that you see in the <head> element of a website is that they are defined as <meta> tags. A tag would have a property and content definition that would follow a pattern shown below.

<meta property="PropertyName" content="PropertyString" />

In the examples that I have seen, they often duplicate some of the information that is already there. In most cases, you would see:

<title>Some Title</title>
<meta property="og:title"
  content="Some Title" />

This makes it fairly effortless to get most of the tags together with only a few complications with dates, images, and whether to support twitters extensions.


A long time ago, some smart people decided that there should be an international standard for representing dates. As of this writing, Hugo doesn’t automatically format the date to this standard.

{{ $iso8601 := "2006-01-02T15:04:05Z07:00" }}

This line defines a string that we can use to later format dates in other parts of the template.

{{- with .PublishDate }}
<meta property="article:published_time" {{ .Format $iso8601 | printf "content=%q" | safeHTMLAttr }} />
{{ end }}

Then when a page is rendered we can see the time and in which time zone.

<meta property="article:published_time" content="2023-03-23T16:50:58-07:00" />


For the home page, we assume that the user wants to use their favicon for their image property.

<meta property="og:image" content="{{ .Site.Author.username }}/favicon.png" />

This works fine for home pages, but we want to provide more information for the articles that may have it.

{{- with .Params.images -}}
{{ "<!-- .Params.images -->" | safeHTML }}
<meta name="twitter:card" content="summary_large_image" />
{{- range first 6 . }}
<meta property="og:image" content="{{ . | absURL }}" />
<meta itemprop="image" content="{{ . | absURL }}" />
{{- end -}}
{{- else -}}
{{ "<!-- $.Site.Params.images -->" | safeHTML }}
<meta property="og:image" content="{{ .Site.Author.username }}/favicon.png" />
<meta itemprop="image" content="{{ .Site.Author.username }}/favicon.png" />
<meta name="twitter:card" content="summary" />
{{- end -}}

The first line checks that images exist in the article.

Line 2 creates a comment in the rendered HTML.

Line 3 is specific to twitter makes the images bigger in the timeline.

Line 4 sets up loop over an array. The key words first 6 limits the loop and the . specifies the array as being the images defined in .Param.images.

Line 5 creates the line of html for that particular image we are on in the loop.

Line 8 starts our logic when there are no images attached to the article and defaults to giving the user profile picture.

Other media

Modern sites can also have audio and visual components as well.

{{- with }}{{ range . }}<meta property="og:audio" content="{{ . }}" />{{ end }}{{ end }}

The line looks at all the audio in the article and then adds the following property.

<meta property="og:audio" content="" />

You can also do the same looping code to go over any video that you might have.

{{- with .Params.videos }}{{- range . }}
<meta property="og:video" content="{{ . | absURL }}" />
{{ end }}{{ end }}

And it will render correctly.

<meta property="og:video" content="" />

At the moment, I don’t have an examples of a system processing this information, but I’m happy knowing that it’s there.

The Twitter Question

As I was working on this, Twitter was acquired by Elon Musk. There were numerous layoffs and I honestly don’t know if I should put in the time to make sure that I support the twitter specific extensions to the OpenGraph specs. Ultimately, I added them in case someone uses this theme and shares the links on Twitter. allows a user to add their twitter name and I used that to populate both twitter:site and twitter:creator.

{{ with .Site.Params.twitter_username }}
<link rel="me" href="{{ . }}" />
<meta name="twitter:site" content="@{{ . }}" />
<meta name="twitter:creator" content="@{{ . }}" />
{{ end }}

If the user doesn’t have a twitter username, this code will not be included during the rendering of the website.


The key difference between OpenGraph and Schema tags is that they use different tags to represent the same information.

A schema meta tag will usually look like the following.

<meta itemprop="PropertyName" content="PropertyString" />

This is basically the same idea but implemented by different groups. When I first learned of Schema tags, I was told that it was used by search engines to parse information. When I attempted to verify this, half the tools used to check have been shutdown or made reference to using OpenGraph.

Still, I wanted to include them as the logic was already completed during my implementation of OpenGraph tags.

In addition to the meta tags, you will see the schema tags in the article itself.

For example, in the <span> tags of the article footers for the different dates and categories.

<span itemprop="articleSection">{{ $Category }}</span>

Currently, I’ve mixed in all the schema tags with the OpenGraph tags to make sure that I have parity between the two where it makes sense.


For open graph testing, I created a page on the test version of my site using the two following tools.


I used to test Schema.


I used to test for accessibility. I did get some alerts with the design because I had some video and audio tags but no transcripts. Another issue is the contrast that I used for the Permalinks on dates. I’ll work on that in a future release.


The twitter official validator is NOTE Removed as of mid 2022. You have to post the link in the composer window of a tweet to see what it will look like.

Opportunities to grow

During the writing of this article, I found numerous ways that I can improve my theme. So much, that I had stopped development because I didn’t want the article and theme to become too separate from each other.

To continue with development without worrying about this, I’ve tagged the version of the code for the time that these steps of the article were created. You can find the link below.

Labarum 1.0.1

In the meantime, I’ll be cleaning up the code so that the Hugo Theme is easier for me to read and that the rendered output as well.

I tested the accessibility of the site, and it did have a decent score, but I’d really like to improve the fonts and colors.

You are welcome to follow this site or the Labarum category to see updates when this is updated. And you can always contact me here or via email!

Creating Labarum Part 1: Design and initialization

Creating Labarum Part 2: Creating the individual posts

Creating Labarum Part 3: Implementing post footer

Creating Labarum Part 4: Initiating the Index

  1. I don’t like the fact that Facebook changed it’s name to meta. ↩︎

  2. Meaning that all the tags are valid html tags. ↩︎