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 |
|
|
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.