86 lines
5.3 KiB
Markdown
86 lines
5.3 KiB
Markdown
|
---
|
||
|
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 }}
|
||
|
<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](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.
|