Automatically generate cover for blog post

Intro
A cover image is a nice way to draw attention to your blog post when sharing a link via social media. However, coming up with and designing a catching cover image can be difficult. It can also distract from the actual content creation.
That’s why I was looking for – and found – a way to automatically generate cover images for blog posts.
These covers contain some basic information for the blog post and displays it in a nice and concise way. Data included in the cover image are the title, author, last update or publish date, tags, and blog post series (if applicable) or category.

Generating the cover
To generate the cover I’m using tcardgen, which I forked to add some custom features.
This tool, written in Go, will generate a cover image with the background, font, and colours of your choosing.
Give the following directory structure
assets/
├── img/
| └── card-template.png
├── font/
| ├── KintoSans-Bold.ttf
| ├── KintoSans-Medium.ttf
| └── KintoSans-Regular.ttf
content/
├── blog/
| └── blog-post
| | └── index.md
you would run the following command to generate a cover image using the default settings.
tcardgen -f assets/font \
-t assets/img/card-template.png \
-o content/blog/blog-post/cover.png \
content/blog/blog-post/index.md
To override the default settings, you can use a config file, like this one:
# tcardgen.config.yaml
template: assets/img/card-template.png
title:
start:
px: 123
py: 165
fgHexColor: "#8B26AF" # Default: "#000000"
fontSize: 68
fontStyle: Bold
maxWidth: 946
lineSpacing: 10
category:
enabled: false # Default: true
start:
px: 126
py: 110
fgHexColor: "#8D8D8D"
fontSize: 42
fontStyle: Regular
info:
enabled: true
start:
px: 240
py: 440
fgHexColor: "#E58003" # Default: "#8D8D8D"
fontSize: 32
fontStyle: Italic
separator: " ∙ "
timeFormat: "Jan 2 '06"
tags:
enabled: true
start:
px: 1025
py: 500 # 451
fgHexColor: "#FFFFFF"
bgHexColor: "#E58003" # Default: "#7F7776"
fontSize: 18
fontStyle: BoldItalic
boxAlign: Right
boxSpacing: 6
boxPadding:
top: 6
right: 10
bottom: 6
left: 10
series: # Added in my custom fork
enabled: true # Default: false
start:
px: 126
py: 110
fgHexColor: "#8D8D8D"
fontSize: 42
fontStyle: Regular
In the config file you can change the colours (text and background) and font style of the different elements on the card. You can also move the location of fields or completely disable them.
In my fork I added the option to disable fields, which got upstreamed, and also added the series taxonomy which I used in my blog.
Use the -c parameter to pass a config file:
tcardgen -c tcardgen.config.yaml \
-f assets/font \
-o content/blog/blog-post/cover.png \
content/blog/blog-post/index.md
Hugo config
By default, Hugo’s built-in templates will use the *cover* located with the blog-post as featured image, unless this is overridden in the frontmatter.
If you name the cover image differently (not feature*, *cover* or *thumbnail*) Hugo will look in the frontmatters .Params.images or your site’s site.Params.images configuration for an image.
For example, you could name the generated cover image after the name of your blog post and then add the following section to the fronmatter:
---
# ...
params:
images:
- blog-post.png
---
Automated cover generation
Now we can manually create a cover for our blogpost by running the tcardgen command and passing the correct parameters.
But it would even be better if we could automate this part.
For this, I set up the following GitHub workflow:
# .github/workflows/create-cover.yml
---
name: Create cover
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches:
- main
- staging
pull_request:
branches:
- main
- staging
# Allow manual trigger
workflow_dispatch: {}
jobs:
changed-files:
name: Check for new posts
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.changed-files.outputs.added_files }}
added_files_count: ${{ steps.changed-files.outputs.added_files_count }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Get new posts
id: changed-files
uses: ./.github/actions/changes
with:
files: |
./content/**.md
matrix: true
matrix-create:
name: Create covers
runs-on: ubuntu-latest
needs: [changed-files]
if: needs.changed-files.outputs.added_files_count > 0
strategy:
matrix:
files: ${{ fromJSON(needs.changed-files.outputs.matrix) }}
max-parallel: 4
fail-fast: false
continue-on-error: true
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: './go.mod' # this will use go.mod to install the correct package(s)
- name: Create cover
run: |
tcardgen -c tcardgen.config.yaml -f assets/fonts -o $(dirname "${{ matrix.files }}")/cover.png ${{ matrix.files }}
# .github/actions/changes/action.yml
name: Changes
description: Check changed files
inputs:
files:
description: Files or folders to monitor
required: false
default: null
files_ignore:
description: Files or folders to ignore
required: false
default: ""
matrix:
description: Output changed files in a matrix-ready format
required: false
default: false
outputs:
any_changed:
description: Returns true when any of the filenames provided using the files or files_ignore inputs have changed. This defaults to true when no patterns are specified. i.e. includes a combination of all added, copied, modified and renamed files
value: ${{ steps.changed-files.outputs.any_changed }}
all_changed_files:
description: Returns all changed files i.e. a combination of all added, copied, modified and renamed files
value: ${{ steps.changed-files.outputs.all_changed_files }}
added_files:
description: Returns only files that are added
value: ${{ steps.changed-files.outputs.added_files }}
added_files_count:
description: Returns number of added files
value: ${{ steps.changed-files.outputs.added_files_count }}
runs:
using: "composite"
steps:
- name: Get changed filed
id: changed-files
uses: tj-actions/changed-files@v45
with:
files: ${{ inputs.files }}
files_ignore: ${{ inputs.files_ignore }}
matrix: ${{ inputs.matrix }}
- name: List all changed files
shell: bash
env:
ANY_CHANGED: ${{ steps.changed-files.outputs.any_changed }}
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
COUNT_NEW: ${{ steps.changed-files.outputs.added_files_count }}
run: |
echo "Files changed (${ANY_CHANGED} - new: ${COUNT_NEW}):"
for file in ${ALL_CHANGED_FILES}; do
echo "- $file"
done
# go.mod
module github.com/TheGroundZero/tcardgen
go 1.23
This workflow will check at every push or PR whether a new blog post was added and – if so – will create a cover image for it.
And we’re done 🚀
Now every blog post will always have a cover image attached. And if I want to use something custom, I can still use the post’s frontmatter to pick it instead of the generator cover.