diff --git a/brainsteam/content/posts/2024/10/cardy-bee.md b/brainsteam/content/posts/2024/10/cardy-bee.md new file mode 100644 index 0000000..f5a968c --- /dev/null +++ b/brainsteam/content/posts/2024/10/cardy-bee.md @@ -0,0 +1,85 @@ +--- +title: "Generating (Non-AI) Social Images with Python and Hugo" +date: 2024-10-29T08:37:07Z +draft: false +description: A short post explaining how I use a Python script to generate (non-ai) social preview images for my site +url: /2024/10/29/cardy-bee +type: posts +mp-syndicate-to: +- https://brid.gy/publish/mastodon +- https://brid.gy/publish/twitter +tags: + - 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](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.