I've grown really tired of manually creating social images for every single blog post. They take way too long to create and online tools always end up looking too generic. How many stock photos can I scroll through before they all start to look the same?
So I built Mugshot Bot. An automated, zero effort social image generator. You pass it a URL and it generates a perfectly sized, unique, beautiful social image.
Here's what they look like! The color and background pattern are randomized from a hand-tuned selection. The title and subtitle come directly from the HTML.
Overall approach
My goal is to design in HTML and CSS and then convert it to a PNG. This worked pretty well with some wkhtmlto*
magic but there were a few hoops I had to jump through. Here's what I did.
Fetch the content
All of the content comes directly from the URL's HTML. So the first step is to fetch the website and parse the DOM. I'm using HTTParty
and Nokogiri
and then looking for specific markup.
body = HTTParty.get(@url).body
html = Nokogiri.parse(body)
title = html.at_css("meta[property='og:title']")
.attr("content")
description = html.at_css("meta[property='og:description']")
.attr("content")
Render and style the HTML
Now that we have the copy we can drop it into some HTML. In Rails we can render an arbitrary view and pass in some variables via ApplicationController#render
.
mugshot = Mugshot.new(title: title, description: description)
rendered_html = ApplicationController.render(
"mugshots/show",
assigns: { title: title, description: description },
formats: [:html],
)
The rendered HTML uses the default layout so we have all of the CSS and fonts normally added in <head>
.
Convert to an image
Where the magic happens: wkhtmlto*
. Or, as it is usually known, wkhtmltopdf
. This library is bundled with a lesser known tool wkhtmltoimage
that does exactly what we need.
If you have the library installed you can call directly into it with Open3
. This works a bit better than backticks because you can handle stderr.
result, error = Open3.capture3(
"wkhtmltoimage jpeg - -",
stdin_data: rendered_html
)
The two dashes (- -
) at the end of the command tell the tool to render from stdin and render to stdout. Open3
will write stdout to result
and stderr
to error
.
Render from the controller
result
is the actual image, as data. We can render this directly from the controller. Ideally, this would be uploaded to S3 and/or put behind a CDN.
def show
# ...
send_data(result, type: "image/jpeg", disposition: "inline")
end
What a weekend!
Thanks for reading, I hope you enjoyed how I built a little side project over the weekend.
If you give Mugshot Bot a try please let me know what you think in the comments! I'm open to feature requests, too.
Top comments (0)