This is a short post describing how I went about adding JSON-LD structured data to my blog posts in Hugo. I expected it to be pretty easy based on my existing experiences modifying my Hugo site, but I ran into some hiccups I didn’t expect.
I started off on this journey because Google’s Search Central has recommendations for structured data, and producing structured data can help change the way Google shows your search results to people who see them. One such recommendation is around Articles or Blog Posts, which was relevant to my site.
What it looks like
The output of JSON-LD in your blog posts is a special script tag with the type=application/ld+json
attribute. Here’s an example of what it looks like for one of my blog posts.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "Weekly Retro 4",
"datePublished": "2024-04-14T21:45:28Z",
"dateModified": "2024-04-14T23:27:53Z",
"author": [{
"@type": "Person",
"name": "0xdade",
"url": "https://0xda.de/"
}]
}
</script>
If you’ve worked with Hugo or Go’s templating engine before, you might be able to see where the problems with this are going to be.
I specifically want to embed parameter content into a script tag, and Go’s templating engine doesn’t want me to do that. In my first attempts, it was converting +
to \u002b
in my timestamps (this ended up being unimportant once I switched date formats) and automatically escaping my {{ site.BaseURL }}
output. This was very annoying and took a while to work around.
Modifying Your Template
To get my Hugo blog posts producing structured data, I had to modify my blog blog/single.html
template and my _default/baseof.html
template.
First I added a simple block to my baseof.html
in the <head>
section, which defines a block for me to specify JSON-LD content.
<head>
{{ partial "head.html" . }}
{{ block "ldjson" . }}{{ end }} <!-- The new line -->
</head>
Then, to get my variables to stop being automatically escaped, I had to use this convoluted printf
scheme to print a script tag with the necessary values inside my blog/single.html
template. This was painful and the sole reason I felt like this deserved a blog post of it’s own.
{{ define "ldjson" }}
{{ printf `<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "%s",
"datePublished": "%s",
"dateModified": "%s",
"author": [{
"@type": "Person",
"name": "%s",
"url": "%s"
}]
}
</script>`
( .Title | markdownify )
( dateFormat .Site.Params.dateform8601 .Date.Local )
( dateFormat .Site.Params.dateform8601 .Lastmod.Local )
( $.Site.Params.author.name )
( .Site.BaseURL )
| safeHTML
}}
{{ end }}
safeJS
and safeJSStr
didn’t work the way I expected them to when trying to embed the variables directly into the script tag, in fact they basically seemed to do nothing, even though every one of them was just a javascript string that I wanted to do nothing else to. The only thing that worked was to printf to construct the whole script tag and then pass that through safeHTML
. If you’re aware of a better way to achieve this, please do reach out and let me know.
Date Format
One other important thing to note is that, at least the way Google expects it, the dates should be formatted in ISO 8601 format. To accomplish this, I added another variable to my config params - dateform8601
. It’s defined like so:
dateform8601 = "2006-01-02T15:04:05Z0700"
This is then used in the above printf
with the dateFormat function, e.g. ( dateFormat .Site.Params.dateform8601 .Date.Local )
. This says to format the localized value of .Date
from each post using the dateform8601
format that is defined in my site params.
After adding this format, I’m inclined to set ISO 8601 as my standard date format across my site. I like standards, and I like ISO 8601 in particular.
Joining the Semantic Web
You should be able to add this to your blog posts pretty easily and join the Semantic Web.