I recently published a new blog post and noticed my Twitter Cards were broken.
What are Twitter Cards you may be wondering? Great question! When you have Twitter Cards configured properly and you share a link on Twitter, you’ll see an image with the Tweet and link to whatever it is you’re sharing.
As someone who is very visual I didn’t like that my Twitter Cards weren’t showing any images. In fact, the link looked boring and I do feel having a catchy image often helps pique curiosity so people want to click the link I share.
I noticed I had Twitter card metadata configured in my Hugo template, but the links being used were relative links (using Hugo’s built in relURL
function). Twitter cards don’t work with relative links - they need to be absolute. While I could just change the pipe to use Hugo’s built in absURL
function, I ended up redesigning the structure of my Hugo website.
At some point in the past few years Hugo introduced the concept of Page Bundles and while I used them on other websites I designed, I never got around to updating this website…until now.
As someone who is very ADHD, having resources local to the corresponding folder is so helpful. Previously I kept all images for all blog posts in a central folder located in my ./static
directory, but I didn’t like this configuration. I preferred to have my images located in the same folder as the blog post they correspond to so I whipped up a janky AF python script and restructured my data.
This meant my post structure now looked like this:
content/
│
├── posts
│ ├── my-post
│ │ │
│ │ ├── image-1.jpg
│ │ ├── image-2.png
│ │ └── index.md
│ └── my-other-post
│ └── index.md
└── another-section
├── foo.md
└── not-a-leaf-bundle
├── bar.md
└── another-leaf-bundle
└── index.md
This new post data structure meant I also had to change the code I used for my Twitter Cards and OpenGraph metadata.
Previously my twitter:image
metadata Hugo code looked like this:
{{ if .IsHome}}
<meta name="twitter:image" content="{{ .Site.Params.hero.hero__image | relURL }}">
{{ else }}
<meta name="twitter:image" content="{{ .Params.Image | relURL }}">
{{ end }}
Because of the new data structure, this wouldn’t work. There are also better ways to handle the scenario of what if .Params.Image
didn’t exist on a particular post, which I previously didn’t handle at all.
Now, Hugo provides an _internal/twitter_cards.html
and _internal/opengraph.html
partial you can use by placing the following shortcode at the top of your head.html
partial for your theme:
{{ template "_internal/opengraph.html" . }}
{{ template "_internal/twitter_cards.html" . }}
However, I discovered this also didn’t work well for me for a few reasons:
- The image specified in the post frontmatter needs to be named
cover
orthumbnail
.- This site has been migrated from Wordpress, to Ghost, and subsequently Hugo so the image names used in past posts are completely random.
- The post frontmatter needs to include
images
instead ofimage
and be in a list or an array format. - If the above criteria isn’t met, then the post frontmatter needs to have a
feature
entry for the desired image. - Someone else owns that shortcode/template and if they make upstream changes, it could break the way Twitter cards and Opengraph card work on my site.
While I could have whipped up a python script to further restructure my data and solve the issue for reasons 1-3, I opted to just write my own Twitter card configuration. Before I jump to the solution, it’s important to understand just why the previous configuration of <meta name="twitter:image" content="{{ .Params.Image | relURL }}">
didn’t work. Let’s take a look at a snippet of my post frontmatter after I moved to page bundles:
---
title: "Twitter Cards with Hugo"
author: "jldeen"
image: "image.png"
layout: post
---
You can see the image:
key is a relative path because the image is now in the same directory as the index.md
file. This means when using {{ .Params.Image | relURL }}
the metadata produced for twitter:image
becomes /image.png
. Now if I update the function to use absURL
, the metadata produced for twitter:image
becomes https://jessicadeen.com/cover.png
. Neither of those URLS are correct or point to the correct location, but that’s okay. We can fix that. Here’s what I came up with:
{{ $siteURL := .Site.BaseURL }}
{{ $relPermalink := .RelPermalink}}
{{- with .Params.share_img | default .Params.image | default .Site.Params.logo }}
{{ $image := . }}
{{- if not (strings.HasPrefix $image "http") }}
{{ $image := urls.JoinPath $siteURL $relPermalink $image }}
<meta name="twitter:image" content="{{ $image }}" />
{{- else }}
<meta name="twitter:image" content="{{ $image }}" />
{{- end }}
{{- end }}
Let’s break this down.
I start off by setting a few environment variables:
siteURL
, which equals the value set in myconfig.yaml
file underBaseURL
relPermalink
, which equals the value of the relative permalink of the current page.
Wrapping this up in a with
function, the code can use .Params.share_img
, .Params.image
or .Site.Params.logo
, and assuming any of those 3 params exist, the next block of code will run.
If the image found does not have an http
prefix, set another variable and use the urls.JoinPath
Hugo function to create a valid URL: baseurl
+ relative permalink
for the current page + image
name found. An example of what that URL would look like is: https://jessicadeen.com/posts/2024/twitter-cards-hugo/cover.png
. Now, in the event the image found does have an http
prefix, great, just use the image link found.
The final piece is of course to test these changes. I could just run hugo serve
and view my site’s metadata which would show something like this:
<meta property="og:title" content="Twitter Cards with Hugo" />
<meta property="og:description" content="Twitter Cards I recently published a new blog post and noticed my Twitter Cards were broken.
What are Twitter Cards you may be wondering? Great question! When you have Twitter Cards configured properly and you share a link on Twitter, you’ll see an image with the Tweet and link to whatever it is you’re sharing.
As someone who is very visual I didn’t like that my Twitter Cards weren’t showing any images." />
<meta property="og:type" content="article" />
<meta property="og:url" content="http://localhost:1313/posts/2024/twitter-cards-hugo/" />
<meta property="og:image" content="http://localhost:1313/posts/2024/twitter-cards-hugo/image.png" />
<meta property="article:section" content="posts" />
<meta property="article:published_time" content="2024-04-04T19:41:09+00:00" />
<meta property="article:modified_time" content="2024-04-04T19:41:09+00:00" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="http://localhost:1313/posts/2024/twitter-cards-hugo/image.png" /><meta name="twitter:title" content="Twitter Cards with Hugo"/>
<meta name="twitter:description" content="Twitter Cards I recently published a new blog post and noticed my Twitter Cards were broken.
What are Twitter Cards you may be wondering? Great question! When you have Twitter Cards configured properly and you share a link on Twitter, you’ll see an image with the Tweet and link to whatever it is you’re sharing.
As someone who is very visual I didn’t like that my Twitter Cards weren’t showing any images."/>
<meta name="twitter:site" content="@jldeen"/>
That’s great, but I’m a visual learner and that’s a lot of HTML to look at very closely. Twitter used to have a Twitter Card validator, but they deprecated that and now suggest you just use the Tweet Composer built into Twitter if you want to see how your Twitter Cards will look. I found that option clunky so I used ThreadCreator’s tool to handle the visual preview/testing:
It also allows you to easily see the metadata found for opengraph and Twitter.
This solution works seamlessly on every page of my site and it handles two key scenarios:
- If it’s the root page of my site, it uses the image specified in my
config.yaml
file under.Params.Logo
. - If it’s a blog post, it uses the image specified in the frontmatter of that specific post.
I then created a custom partial called twitter.html
, updated the Twitter metadata accordingly, and placed it in my layouts/partials
folder. Then, in my head.html
file, I included the partial by calling {{ partial "partials/twitter.html" . }}
. I did the same for OpenGraph.
This approach gives me full control over the Twitter Card configuration, eliminates the risk of any breaking changes that could be caused by upstream Hugo updates, and avoids any relURL
or absURL
issues.