Generate and display OpenGraph images
This post inspired me to write this article:
Previously I wrote about setting Meta tags in a Rails app. Meta tags really make your web pages more “shareable”.
But generating OpenGraph images can be a challenge. There are many businesses built around this. For example:
But you are a great developer! You don’t need to pay for a tool that you can just build, right?
Let’s build an OpenGraph image generator!
We could use Rmagick to draw images with their API.
But maybe an easier approach would be to generate some HTML, open it in a browser, and take a screenshot.
For this we will use Gem Ferrum that is a headless Chrome API.
There are a few levels of coolness/complexity for taking screenshots of an url:
- Level 1.1. Take a screenshot of an URL
Level 1.2. Take a screenshot of an URL selector (id/class)- Level 2.1. Take a screenshot of an URL with a dedicated template
Level 2.2. Visit web page, parse the meta%i[title, description, logo, date]
& autocomplete your generic template
Level 1.1. Take a screenshot of an URL #
Example of final result - a screenshot of a page:
We will leverage the Ferrum Screenshot API:
# rails g job UrlToImage
# url = "https://superails.com/posts/181-search-and-autocomplete-french-company-information"
# UrlToImageJob.perform_now(url)
class UrlToImageJob < ApplicationJob
queue_as :default
def perform(url)
browser = Ferrum::Browser.new
browser.resize(width: 1200, height: 630)
browser.goto(url)
# browser.screenshot(path: "tmp/screenshots/#{url.parameterize}.jpg")
# browser.screenshot(path: "tmp/screenshots/#{url.parameterize}.jpg", quality: 40, format: "jpg")
# browser.screenshot(path: "tmp/screenshots/#{url.parameterize}.jpg", quality: 40, format: "jpg", full: true)
# sleep 0.5
# browser.screenshot(path: "app/assets/images/opengraph/#{url.parameterize}.jpg", quality: 40, format: "jpg", selector: "main")
browser.screenshot(path: "app/assets/images/opengraph/#{url.parameterize}.jpg", quality: 40, format: 'jpg')
# tempfile = Tempfile.new
# browser.screenshot(path: tempfile.path, format: 'jpg', quality: 40)
# Post.first.image.attach(io: File.open(tempfile.path), filename: "#{post.url.parameterize}.jpg")
ensure
browser.quit
end
end
ℹ️ Generating JPEG can be faster than PNG.
And here’s a helper to access the generated image based on the current URL:
# app/helpers/application_helper.rb
def meta_opengraph_image_asset_path
base_url = "https://superails.com"
image_name = [base_url, request.path].join.parameterize
full_path = "opengraph/#{image_name}.png"
helpers.image_url(full_path)
rescue StandardError
image_url('logo.png')
end
Display the image in meta tags
# app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
+ <%= yield :head %>
</head>
</html>
# posts/show.html.erb
<%= content_for :head do %>
<%= tag.meta(property: 'og:image', content: meta_opengraph_image_asset_path) %>
<%#= tag.meta(property: 'og:image', content: rails_blob_url(@event.og_image)) %>
<%= tag.meta(property: 'twitter:card', content: "summary_large_image") %>
<% end %>
This way we store the image in our assets. It is for you to decide a better way to deliver these images to production.
Level 2.1. Take a screenshot of an URL with a dedicated template #
Example of final result:
Instead of visiting an URL, we can just render plain HTML in Ferrum and take a screenshot of it:
# https://github.com/rubycdp/ferrum/blob/main/lib/ferrum/frame.rb#L109
browser = Ferrum::Browser.new
browser.resize(width: 1200, height: 630)
frame = browser.frames.first
frame.body
# => "<html><head></head><body></body></html>"
frame.content = "<html><head></head><body>Voila!</body></html>"
frame.body
# => "<html><head></head><body>Voila!</body></html>"
Now we can create a layout and template for our open graph images!
A minimalistic CSS-only layout:
<!-- app/views/layouts/minimal.html.erb -->
<!DOCTYPE html>
<html>
<head>
<title>Og</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body style="background: linear-gradient(to right, #fdf497, #fdf497, #fd5949, #d6249f, #285AEB); display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0;">
<div style="transform: rotate(4deg);">
<%= yield %>
</div>
</body>
</html>
A template for generating post images:
<!-- app/views/posts/og.html.erb -->
<div style="border: 1px solid #ddd; padding: 2rem; background-color: #fff; border-radius: 15px; margin: 2rem; display: flex; flex-direction: column; align-items: center; gap: 1rem; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); max-width: 600px; margin: auto;">
<div style="height: 80px; margin-bottom: 1rem;">
<img src="http://localhost:3000/logolong.png" height="80">
<%#= image_tag asset_url('logo.png'), style: 'width: 100%; height: 80px;' %>
</div>
<div style="word-break: break-word; font-weight: bold; font-size: 2rem; text-align: center; color: #333;">
<%= @post.title %>
</div>
<div style="width: 100%; display: flex; justify-content: center; margin-bottom: 1rem;">
<%= image_tag local_image_url(@post), style: 'max-width: 100%; height: auto; border-radius: 10px;' %>
</div>
<div style="font-size: 0.8rem; color: #777; font-style: italic; font-weight: bold;">
<%= @post.published_at.strftime('%B %d, %Y') %>
</div>
</div>
Notice the asset_url
& local_image_url
. Ferrum needs absolute, not relative image urls to display images
def local_image_url(post)
"http://localhost:3000#{rails_blob_path(post.image)}"
# "http://localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MSwicHVyIjoiYmxvYl9pZCJ9fQ==--8a5968eca5f43d315dfe06b402d555eddbcbc994/https-www-railsconf-com.jpg"
# url_for(post.image)
# rails_blob_url(post.image)
# "#{request.base_url}#{rails_blob_path(post.image)}"
end
Finally, here’s the job to generate a screenshot from the above HMTL:
# post = Post.first
# PostToImageJob.perform_now(post)
class PostToImageJob < ApplicationJob
queue_as :default
def perform(post)
@post = post
html = ApplicationController.render(
template: 'posts/og',
layout: 'minimal',
assigns: { post: @post })
browser = Ferrum::Browser.new
browser.resize(width: 1200, height: 630)
frame = browser.frames.first
# frame.content = "<html><head></head><body>Voila!</body></html>"
frame.content = html
# ensure all images are loaded!
browser.network.wait_for_idle
# double check if all images are loaded!!
sleep 1
browser.screenshot(path: Rails.root.join('tmp', 'screenshot.png'))
browser.screenshot(path: "app/assets/images/opengraph/posts/#{post.id}.png", quality: 40, format: 'jpg')
# tempfile = Tempfile.new
# browser.screenshot(path: tempfile.path, format: 'jpg', quality: 40)
# Post.first.og_image.attach(io: File.open(tempfile.path), filename: "#{post.url.parameterize}.jpg")
ensure
browser.quit
end
end
That’s it! 🤠
Related resoures:
Did you like this article? Did it save you some time?