Alexito's World

A world of coding 💻, by Alejandro Martinez

Publish step to generate social images for your posts

In this post I want to show you a little behind the curtain. As you may have noticed if you follow me on Twitter, recently the tweets with links to this post got a little fancier. What you may not know is that these images are generated using SwiftUI.

Twitter card example

I've wanted to have fancy poster images on my posts for a while, but I didn’t want to have to make them manually, or even manage them at all. Since this website is built using Publish it means I already have all the power of Swift available. And because I run the generation on macOS, it means I also have access to SwiftUI. So why not take advantage of that and use SwiftUI to generate the images for every post?

From SwiftUI views to images

Getting images from a SwiftUI view is the first problem to solve. I will not discuss it much here because I already wrote a whole post about it. I also made a Swift Package that encapsulates this functionality so we can skip directly into how to bend Publish to our needs.

Vanilla Publish

To start this process, I will assume we're working with a vanilla installation of Publish. I have some tweaks and changes on the tool to facilitate some things, that I need for this website. But I will stick to the basics for this post.

The first thing to understand is that Publish already comes with support incorporate this type of images on your post. The Item has an imagePath property and if you check the provided Plot components; you see that the default head adds the twitter card as follows:

.twitterCardType(location.imagePath == nil ? .summary : .summaryLargeImage),

That is very good! It means we just want to worry about generating the images and setting that path.

The step

To avoid having to make the images manually we will add a step that creates all the images for our posts.

additionalSteps: [
    .generatePosters()
]

The structure of the step is quite typical in any Publish step:

try context.mutateAllSections { section in
		try section.mutateItems { item in
				...

We need to iterate over all items of all sections to perform the work. Note that we are using the mutating variants because once we have the poster generated we want to set its path to the item imagePath property so the theme can pick it up.

The rough plan for the work is as follows:

  1. Skip the generation when appropriate
  2. Generate the poster image
  3. Save the image
  4. Assign the path to the item's image property

Avoid the work

We want to avoid doing the work for multiple reasons. The first check we should do is to detect if the item already has an imagePath set. That indicates that the item already has an image associated, we don't want to overwrite that.

guard item.imagePath == nil else {
    return
}

The second reason to avoid doing the work is performance. Generating views and rendering an image out of them can be costly, especially if the site contains many posts. That's why it should only be done once and just change the image if the data used on the view changes.

Unfortunately, I haven't found a pleasant way of doing this in vanilla Publish. The context has a method to create or retrieve a cached file cacheFile(named:) but I haven't found a convenient way of knowing if the file was just created or already existed.

In my website I don't use the cache because I persist the images in a different way. See below.

Generate the image

Since we already have a package to generate the image, the only thing we need is to build the actual SwiftUI view. This is what everybody can customise to best fit the style of the website. For this exercise, I’ve just created a very basic view that displays the title of the post.

struct Poster: View {
    let title: String
    
    var body: some View {
        Text(title)
            .font(.system(size: 200))
            .bold()
            .background(Color.gray)
    }
}

The view needs to accept the data that it needs, usually data from the post or the website. In this example I'm just using the title of the post but you can use the date, tags, etc.

I recommend you to design your view to be rendered at big sizes, this will give more resolution to the final image. In the example I use a big font size to accomplish that.

With the SwiftUI view created, we can just use Raster to make an image out of it.

let size = CGSize(width: 1600, height: 840)
let image: NSBitmapImageRep = Poster(title: item.title).rasterizeBitmap(at: size)
let data = image.representation(using: .jpeg, properties: [:])

Save the image

This is probably the most problematic step of all. It should be simple, after all we just have to save the Data in disk. But the question is, where exactly?

In my site I treat the posters as Content and that's why I save them alongside the markdown file. That works nicely because I don't have the markdown files all in the same folder. Instead, each post has its own folder with its own resources, what we often call a "bundle". After many years of working with static site generators, I definitely think this is the best approach. That's why I just save the file in the post bundle and everything is packaged nicely.

But in a vanilla Publish installation we probably don't want that. The more basic scenario is to just save the file directly in the output directory. That's easy since we just have to combine the item path with a name for the poster image. But keep in mind that this will slow down generation.

let posterPath = item.path.appendingComponent("poster.jpg")
let file = try localContext.createOutputFile(at: posterPath)
try file.write(data)

Use the poster

To use the poster we just have to assign the path of the image to the item, a supported property of Publish's Item type.

item.imagePath = posterPath

And because we're using the mutate APIs they will propagate this change to the next steps of the pipeline. That's going to let the theme use this path to create the proper HTML.

Conclusion

As you can see, the work is not very complex once you have a way to create an image from a view. The rest is working with Publish API to get the desired effect. For some things the API is a bit too restricted and you need to workaround it, but it's all possible.

If you want to use this system on your own site, you can check out the plugin that I open sourced: ItemPosterPublishPlugin. By default the plugin works as explained on this post, but it's fully customisable with closures in order to be as flexible as posible. This allows me to use the same plugin on my site, which has a different behaviour.

I would love for Publish to support officially bundled posts, a better way to detect the local path of an item and better cache interaction. With these things I could make the plugin way better by default. I understand that all of these may be requirements for a narrow use case but these things are almost the only ones that forced me to maintain my own fork of the tool.

In any case I hope you enjoyed this post and the Plugin. If you integrate this on your website be sure to mention me on Twitter so I can see your fancy post images!

If you liked this article please consider supporting me