--- date: 2024-10-29 08:37:07+00:00 description: A short post explaining how I use a Python script to generate (non-ai) social preview images for my site draft: false mp-syndicate-to: - https://brid.gy/publish/mastodon - https://brid.gy/publish/twitter preview: /social/ceb86a1341e97b030d5701fe9ec41422cb237160fbd174c1911a1a34b70da703.png tags: - colophon - blogging - pompidou - hugo - python title: "Generating (Non-AI) Social Images with Python (Cardy \U0001F41D) and Hugo" type: posts url: /2024/10/29/cardy-bee --- 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](https://www.eliostruyf.com/generate-open-graph-preview-image-code-front-matter/). I reimplemented his typescript/nodejs tool in Python using the [html2image](https://pypi.org/project/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](https://git.jamesravey.me/ravenscroftj/cardy-bee) 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 }} {{ 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](https://runtimeterror.dev/dynamic-opengraph-images-with-hugo/) 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. ## Final Thoughts This experiment was pretty quick for me to knock up and I'm pretty happy with the results. I've made the script available to others just in case it is useful but I'm not sure how useful it will be. You might be better off forking it and tailoring it to your needs or just using it as inspiration. Apologies for the name, I am a big fan of dad jokes, what can I say?