brainsteam.co.uk/brainsteam/content/posts/2024/10/cardy-bee.md

5.3 KiB

title date draft description url type mp-syndicate-to tags
Generating (Non-AI) Social Images with Python and Hugo 2024-10-29T08:37:07Z false A short post explaining how I use a Python script to generate (non-ai) social preview images for my site /2024/10/29/cardy-bee posts
https://brid.gy/publish/mastodon
https://brid.gy/publish/twitter
colophon
blogging
pompidou

These days if you link to a blog post from a social media site like Mastodon or BlueSky the site will load your page and look for metadata element tagged og:image and render it inline as part of the link out to the article. If you don't supply one of these images, the social site will typically just show a grey rectangle or a "broken image" icon. Therefore, most of the time, when posting a new article I try to set a "feature image" to avoid this ugliness.

I know a lot of people are using AI-generated images as featured images and I did have a brief flirtation with this last year but I've gone right off that idea.

Instead, I was inspired to generate title cards for my posts. I read a few different posts on the topic but ended up largely following Elio Struyf's approach. I reimplemented his typescript/nodejs tool in Python using the html2image library. I am using a Python script to launch Chrome into headless mode and render static screenshots of the title card ( I am using a modified version of Elio's HTML template). Then I'm storing these files in my hugo static images directory and adding a metadata entry to each blog post to tell it where it's corresponding OG image lives.

Cardy-🐝 image generation script

My script, cardy-bee is very bespoke to my hugo setup but it is available under the GPL license from here The script runs every time a change gets made to my website repo (which would indicate that I've added a new article or edited/removed an existing one).

I don't version control the images since they can be reliably re-generated automatically. I do cache them by generating a hash of the post title and publication date and using that for the image filename. The script quickly checks all existing posts to see if they have a corresponding title card set in the hugo front matter and whether that file exists. It will then generate the image as necessary.

If there is a change to the title or date then the hash will change and the generated filename will be different. This should trigger the script to automatically generate a new title card and if the old file still exists, it will rename it out of the way to old_name.png.cleanup. Then I can just run rm *.cleanup periodically to get rid of old thumbnails. I could have fully automated this step but I like the idea of being able to go in and check/restore old thumbnails.

Each image is about 42kb in size so they're not horrendous to keep around (and they should load pretty quickly over slow-ish connections too).

Adding the Images to Hugo

By default cardy-🐝 will store the image name in the hugo post frontmatter under the preview attribute. After that, I've got a rather ugly snippet in my Hugo template which checks for different possible images in the frontmatter. Firstly we check for a preview attribute and if that's not there we check for other possible image metadata (my micropub tool generates photo entries when I upload pictures I've taken).

  {{ $ogImage := "" }}
  {{ with .Params.image }}
    {{ $ogImage = . | absURL }}
  {{ else }}
    {{ with .Params.preview }}
      {{ $ogImage = . | absURL }}
    {{ else }}
      {{ with .Params.photo }}
        {{ if reflect.IsSlice . }}
          {{ range first 1 . }}
            {{ if isset . "url" }}
              {{ $ogImage = .url | absURL }}
            {{ else if isset . "value" }}
              {{ $ogImage = .value | absURL }}
            {{ else }}
              {{ $ogImage = . | absURL }}
            {{ end }}
          {{ end }}
        {{ else if reflect.IsMap . }}
          {{ if isset . "url" }}
            {{ $ogImage = .url | absURL }}
          {{ else if isset . "value" }}
            {{ $ogImage = .value | absURL }}
          {{ end }}
        {{ else }}
          {{ $ogImage = . | absURL }}
        {{ end }}
      {{ else }}
        {{ with .Resources.ByType "image" }}
          {{ range . }}
            {{ if in .Name "feature" }}
              {{ $ogImage = .Permalink }}
              {{ break }}
            {{ end }}
          {{ end }}
        {{ end }}
      {{ end }}
    {{ end }}
  {{ end }}

  {{ if $ogImage }}
    <meta property="og:image" content="{{ $ogImage }}" />
  {{ end }}

Could I have done this more efficiently?

For sure, I mean anything that requires you to run headless-chrome is not exactly going to be lightweight. I noticed that this post uses Hugo's built in image manipulation features to take an image template and overlay some text onto it rather than booting up a whole browser. I went for lowest barrier to entry/quickest win and I'm not too worried about the resource that running this script once every few days when I hit "publish" is going to require. I'll revisit this if it ever does become an issue or if I feel like deep diving on Hugo's image generation stuff.