If we have a site that publishes a considerable amount of content, we usually need to generate the assets that go with each piece of content.
For example, if it's a blog post like this one, we might need a cover, diagrams, screenshots, etc.
However, sometimes we neglect the Open Graph image, even if it's arguably one of the most important assets: it's what people see before they decide to read our content or not.
In this article we will learn how to generate Open Graph images with Ruby in a Rails application and how to automate the process using one or more templates.
Let's start by giving a quick glance at what Open Graph is and why you should care about it. If you already know about it, skip to the application setup section.
What is Open Graph
Open Graph is a protocol that uses metadata, specifically: data contained in meta tags, that controls how a webpage appears when its URL is shared on social media.
It was originally developed by Facebook but the protocol is widely used by many other platforms, pages and even mobile applications.
A basic OG tag looks like the following:
<meta name="og:title" content="Open Graph Image Generation with Ruby" />
As you can see, it's a meta tag but it uses the og
prefix for each one of the attributes which can be title
, description
, url
, type
, among others.
An OG image tag, looks like this:
<meta name="og:image" content="https://d2g0xdrrde9ln1.cloudfront.net/d9otbdv362gs4d3cov13zgw47r98" />
If we share a page that doesn't have specific Open Graph metadata, most platforms will attempt to retrieve and generate a share card using other meta tags like the page title or description and some image that may appear relevant.
But we would rather not leave the way our content looks when shared on social media to the platform itself, we may not like the results.
So, to control the way our webpages look when shared on social media, at the very least we can customize the title and description by providing custom og:title
and og:description
values.
However, the real magic happens when we add a custom image that's well designed and can help our content stand out and encourage users to click on our content previews.
What we will build
To demonstrate how to add this feature to a Rails application, we will work on a simple blogging application that has an Article
model with a title
, body
and excerpt
fields.
Using the title
we will generate a custom open graph image after an article is created, upload it using Active Storage so we can access it anytime and then add the image as an OG tag.
To generate the image, we can use two different approaches:
- Using ImageMagick with the MiniMagick gem: this approach is powerful but a bit more convoluted. We can't use CSS, and achieving the desired text and image positioning can be a bit tedious. However, it is best suited to produce programmatic images with many moving parts that are more difficult to customize with CSS.
- Using Ferrum as a headless browser: using Ferrum we can render HTML/CSS and screenshot the result using a headless browser. This gives us the flexibility and familiarity of using CSS, but it has some disadvantages like the fact that we need to have Chrome/Chromium installed and that it can be a bit overkill for simple image generation.
For this tutorial, we will use both approaches so we can explore the pros and cons of each one of them in more depth.
We will start with a design that looks like this:

And implement it with both methods so we can see how to implement the feature considering real-world constraints like a design specification.
Now that we know what we will be working on, let's move to the application setup.
Application Setup
Let's start by creating a new Rails application:
rails new og_image --css=tailwind --javascript=esbuild
Next, let's install Avo for our Rails Admin panel so we can easily create and work with the Article
resource and any other resource we might need in the future:
bundle add avo && bundle install
We then run the Avo installation command which will mount the admin routes at /avo
and generate an avo.rb
configuration initializer:
bin/rails generate avo:install
Now, let's install Active Storage:
bin/rails active_storage:install
And run the recently created migrations:
bin/rails db:migrate
We can now create the Article
model by running the generate
command which will also add an article.rb
resource that we can use with Avo:
bin/rails generate model Article title excerpt body:text
We then add a validation to make sure every article has a title
and add attachments for the article's cover
and og_image
:
# app/models/article.rb
class Article < ApplicationRecord
has_one_attached :cover
has_one_attached :og_image
validates :title, presence: true
end
Now, we make sure to add the cover
field to the article.rb
Avo resource:
# app/avo/resources/article.rb
class Avo::Resources::Article < Avo::BaseResource
def fields
field :id, as: :id
field :title, as: :text
field :excerpt, as: :text
field :body, as: :textarea
field :cover, as: :file
end
end
We then run the migration:
bin/rails db:migrate
Now, we should be able to navigate to /avo/articles
and see the following:

Before continuing with our task, we will be using the Satoshi font that you can download here for free.
After downloading it and decompressing it, let's move them to an app/assets/fonts
folder so we can access them from our code.
We now have everything set up, we can start with the image generation using the MiniMagick gem:
OG image using MiniMagick
If you haven't used it before, MiniMagick is basically a low-memory replacement for RMagick, which is the go-to Ruby gem to interact with ImageMagick., a popular CLI image manipulation library.
ImageMagick allows us to perform many operations with images using a command line tool which is why it's widely used to dynamically generate images.
We need to make sure it is installed so let's start by running the following command:
magick --version
Which should return something like this:
Version: ImageMagick 7.1.2-5 Q16-HDRI aarch64 23392 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI Modules OpenMP
Delegates (built-in): bzlib fontconfig freetype heic jng jp2 jpeg jxl lcms lqr ltdl lzma openexr png raw tiff webp xml zip zlib zstd
Compiler: clang (16.0.0)
If it doesn't, please refer to the installation instructions for your operating system and make sure the magick
command is working before continuing.
Let's start by installing the mini_magick
gem:
bundle add mini_magick && bundle install
Now we can create a class that can receive an Article
instance and create an OG image based on the design specifications.
Let's add it in the models directory and start by generating a 1200×630 PNG that has the blue gradient that goes from the top left to the bottom right:
# app/models/og_image_generator.rb
class OgImageGenerator
OG_WIDTH, OG_HEIGHT = 1200, 630
attr_reader :article
def initialize(article)
@article = article
end
def generate
image_path = Rails.root.join("tmp", "og_image_#{article.id}.png")
create_gradient_background(image_path)
image_path
end
private
def create_gradient_background(path)
MiniMagick.convert do |convert|
convert.size "#{OG_WIDTH}x#{OG_HEIGHT}"
convert.define "gradient:direction=NorthWest"
convert << "gradient:#2E8CF0-#53BDFF"
convert << path
end
end
end
Here, we define a generate
method where we will call the individual actions that perform most of the work.
In this case, the create_gradient_background
method creates an image that has the dimensions we need with a gradient that goes from the top left to the bottom right corner of the image.
The MiniMagick library abstracts the ImageMagick command that's needed to generate the image we want in a nice and Ruby friendly manner.
The resulting image looks like this:

Certainly nothing to write home about but we're on our way. Let's continue our journey by adding the digital noise image on top of the gradient using the add_noise_overlay
method which, in turn, uses the ImageMagick composite
method, used to merge two or more images using a blending mode.
Let's add the method:
class OgImageGenerator
attr_reader :article
OG_WIDTH, OG_HEIGHT = 1200, 630
def initialize(article)
@article = article
end
def generate
image_path = Rails.root.join("tmp", "og_image_#{article.id}.png")
create_gradient_background(image_path)
add_noise_overlay(image_path)
image_path
end
private
# Rest of the code
def add_noise_overlay(image_path)
noise_path = Rails.root.join('app', 'assets', 'images', 'bit-noise.png')
MiniMagick.convert do |convert|
convert << image_path
# Stack creates the parentheses: \( ... \)
convert.stack do |stack|
stack << noise_path
stack.resize "#{OG_WIDTH}x#{OG_HEIGHT}!"
stack.alpha "set"
stack.channel "A"
stack.evaluate "set", "6%"
end
convert.compose "multiply"
convert.composite
convert << image_path
end
end
end
When we run this, we get the following result:

We're making some progress. Let's start by adding the Avo logo located at the top left corner of the image with a margin of 80px to the borders.
To achieve this, we need to load the logo image, resize it and then generate a composite image with the logo on it
class OgImageGenerator
# Rest of the code
def add_logo(path)
logo_path = Rails.root.join("app", "assets", "images", "logo-white.png")
base_image = MiniMagick::Image.open(path)
logo_image = MiniMagick::Image.open(logo_path)
logo_image.resize "96x"
result = base_image.composite(logo_image) do |c|
c.geometry "+80+80"
end
result.write(path)
end
end
The result looks like this:

The next step is to add the text for the title. Let's start with a naive approach where we will display the text without considering the amount of lines:
class OgImageGenerator
# Rest of the code
def add_title_text(image_path)
lines = wrap_text
image = MiniMagick::Image.open(image_path)
image.combine_options do |c|
c.font font_path("black")
c.fill "white"
c.pointsize "64"
c.antialias
c.draw "text 80,315 '#{article.title}'"
end
image.write(image_path)
end
def font_path(weight = "regular")
name = "Satoshi-#{weight_string(weight)}.otf"
Rails.root.join("app", "assets", "fonts", name)
end
def weight_string(weight)
{
300 => "Light",
"light" => "Light",
400 => "Regular",
"regular" => "Regular",
500 => "Medium",
"medium" => "Medium",
700 => "Bold",
"bold" => "Bold",
900 => "Black",
"black" => "Black"
}[weight]
end
end
This produces the following result:

The typography is right so we know that it's loading correctly but, the text overflows the image and is not like the text in the sample design.
To solve this, we need to generate a method that splits the text into an n
amount of string elements in an array.
If we check the design specification, the max length for a line is 20 characters long so let's add a wrap_text
method that accepts a text
and max_length
as arguments and returns an array of lines that we can use to iterate over to generate our wrapped text.
class OgImageGenerator
# Rest of the code
def wrap_text(text, max_length = 20)
words = text.split
lines = []
current_line = []
words.each do |word|
test_line = (current_line + [word]).join(" ")
if test_line.length > max_length && current_line.any?
lines << current_line.join(" ")
current_line = [word]
else
current_line << word
end
end
lines << current_line.join(" ") if current_line.any?
lines
end
end
What this method does is define a words
array together with a lines
and current_line
empty arrays.
Then it iterates over the list of words, defining a test_line
variable that joins the current_line
and each word
and then tests to see if the test_line
exceeds the max_length
which is predefined at 20 characters.
If the test_line
exceeds the length, it saves the current_line
to the lines
array and starts a new line with the current word. Otherwise, it simply adds the word to the current_line
.
Finally, after processing all words, it adds the last current_line
to lines
if it contains any words, and returns the array of wrapped lines.
In other words, this method produces an array of words where each element of the array doesn't exceed the max length we previously defined.
Now, we can modify our add_title_text
method to call the wrap_text
and generate the lines that we iterate to add the title
line by line:
class OgImageGenerator
# Rest of the code
def add_title_text(image_path)
lines = wrap_text(article.title)
image = MiniMagick::Image.open(image_path)
lines.each_with_index do |line, index|
y_position = 250 + (index * 72)
image.combine_options do |c|
c.font font_path("black")
c.fill "white"
c.pointsize "64"
c.antialias
c.draw "text 80,#{y_position} '#{line}'"
end
image.write(image_path)
end
end
end
Now, if we run the generator with this incorporated:
article = Article.first
gen = OgImageGenerator.new(article)
gen.generate
We get the following result:

We've made some progress now! Let's add a method that adds the domain text:
class OgImageGenerator
# Rest of the code
def generate
# Rest of the calls
add_domain_text(image_path, "avohq.io")
end
def add_domain_text(image_path, domain)
image = MiniMagick::Image.open(image_path)
image.combine_options do |c|
c.font font_path("bold")
c.fill "#FFFFAA"
c.pointsize 36
c.antialias
c.gravity "southwest"
c.draw "text 80,80 '#{domain}'"
end
image.write(image_path)
end
end
Here, we're adding a text at the 80,80
position with a southwest gravity which means it is positioned 80 pixels from the left and 80 pixels from the bottom of the image.
After we run the generator once again, we get the following result:

We're already there, our resulting image looks just like the design but, for the sake of it, let's add author information where the domain text and move the domain to the right of the image.
To avoid adding unnecessary code, let's hardcode the values for the author name and avatar to the Article
model:
class Article < ApplicationRecord
# Rest of the code
def author_name
"Exequiel Rozas"
end
def author_avatar_url
"https://avatar.iran.liara.run/public/16"
end
end
Next, let's add a method to add an author avatar and the name:
class OgImageGenerator
# Rest of the code
def generate
# Rest of the calls
add_author_info(image_path)
end
def add_author_info(path)
add_avatar(path, article.author_avatar_url)
add_author_name(path, article.author_name)
end
def add_avatar(image_path, avatar_url)
base_image = MiniMagick::Image.open(image_path)
avatar_image = MiniMagick::Image.open(avatar_url)
avatar_image.resize "48x48"
result = base_image.composite(avatar_image) do |c|
c.gravity "southwest"
c.geometry "+80+80"
end
result.write(image_path)
end
def add_author_name(image_path, author_name)
image = MiniMagick::Image.open(image_path)
avatar_size = 48
font_size = 24
padding = 16
# Calculate position next to avatar, vertically centered
author_x = 80 + avatar_size + padding
author_y = 80 + (font_size / 2)
image.combine_options do |c|
c.font font_path("bold")
c.fill "white"
c.pointsize font_size
c.interline_spacing 0
c.antialias
c.gravity "southwest"
c.draw "text #{author_x},#{author_y} '#{author_name}'"
end
image.write(image_path)
end
end
What's happening here is that we're adding an avatar that measures 48x48
starting at the bottom left 80px position.
Then, we add the author_name
that we take from the article hardcoded value and locate it at 80 pixels from the bottom and add the font size divided by two to locate it.
The result looks like this:

We have achieved our goal of building an open graph image using Ruby. Let's add the ability to attach the image to the Article
using Active Storage by using a job so the attachment doesn't get stuck if anything goes wrong.
class AttachImageToArticleJob < ApplicationJob
queue_as :default
def perform(article_id, image_path)
article = Article.find(article_id)
article.og_image.attach(
io: File.open(image_path),
filename: File.basename(image_path),
content_type: Marcel::MimeType.for(Pathname.new(image_path))
)
end
end
Now, we can invoke the job when generating the image:
class OgImageGenerator
# Rest of the code
def generate
# Rest of the calls
attach_to_article(image_path)
end
def attach_to_article
AttachImageToArticleJob.perform_later(article.id, path.to_s)
end
end
Note that we call to_s
on the image path as the job class expects a string and not a Pathname
instance.
Now, we can create a job to perform the image generation and add that to the Article
callbacks:
# app/jobs/create_og_image_job.rb
class CreateOgImageJob < ApplicationJob
queue_as :default
def perform(article_id)
article = Article.find(article_id)
OgImageGenerator.new(article).generate
end
end
The feature is now complete, if we test this we get the following result:
Once we know that everything's working correctly, we can improve the performance by avoiding unnecessary writes to disk so I joined methods that perform similar tasks.
The resulting code is the following:
# app/models/og_image_generator.rb
class OgImageGenerator
attr_reader :article
OG_WIDTH, OG_HEIGHT = 1200, 630
GRADIENT_COLORS = ["#2E8CF0", "#53BDFF"]
def initialize(article)
@article = article
@font_paths = {}
end
def generate
image_path = Rails.root.join("tmp", "og_image_#{article.id}.png")
create_gradient_with_noise(image_path)
add_logo_and_avatar(image_path)
add_all_text(image_path)
attach_to_article(image_path)
image_path
end
private
def create_gradient_with_noise(path)
noise_path = Rails.root.join('app', 'assets', 'images', 'bit-noise.png')
MiniMagick.convert do |convert|
convert.size "#{OG_WIDTH}x#{OG_HEIGHT}"
convert.define "gradient:direction=NorthWest"
convert << "gradient:#{GRADIENT_COLORS[0]}-#{GRADIENT_COLORS[1]}"
convert.stack do |stack|
stack << noise_path
stack.resize "#{OG_WIDTH}x#{OG_HEIGHT}!"
stack.alpha "set"
stack.channel "A"
stack.evaluate "set", "6%"
end
convert.compose "multiply"
convert.composite
convert << path
end
end
def add_logo_and_avatar(image_path)
base_image = MiniMagick::Image.open(image_path)
logo_image = MiniMagick::Image.open(Rails.root.join("app", "assets", "images", "logo-white.png"))
avatar_image = MiniMagick::Image.open(article.author_avatar_url)
logo_image.resize "96x"
avatar_image.resize "48x48"
base_image = base_image.composite(logo_image) do |c|
c.geometry "+80+80"
end
base_image = base_image.composite(avatar_image) do |c|
c.gravity "southwest"
c.geometry "+80+80"
end
base_image.write(image_path)
end
def add_all_text(image_path)
image = MiniMagick::Image.open(image_path)
lines = wrap_text(article.title)
image.combine_options do |c|
c.font font_path("black")
c.fill "white"
c.pointsize "64"
c.antialias
lines.each_with_index do |line, index|
y_position = 250 + (index * 72)
c.draw "text 80,#{y_position} '#{escape_title(line)}'"
end
c.font font_path("bold")
c.fill "#FFFFAA"
c.pointsize 36
c.gravity "southeast"
c.draw "text 80,80 '#{escape_title("avohq.io")}'"
c.fill "white"
c.pointsize 24
c.gravity "southwest"
author_x = 80 + 48 + 16
author_y = 80 + 12
c.draw "text #{author_x},#{author_y} '#{escape_title(article.author_name)}'"
end
image.write(image_path)
end
def wrap_text(text, max_length = 20)
words = text.split
lines = []
current_line = []
words.each do |word|
test_line = (current_line + [word]).join(" ")
if test_line.length > max_length && current_line.any?
lines << current_line.join(" ")
current_line = [word]
else
current_line << word
end
end
lines << current_line.join(" ") if current_line.any?
lines
end
def attach_to_article(path)
AttachImageToArticleJob.perform_later(article.id, path.to_s)
end
def escape_title(title)
title.to_s.gsub("'", "\\\\'").gsub('"', '\\"')
end
def font_path(weight = "regular")
@font_paths[weight] ||= begin
name = "Satoshi-#{weight_hash(weight)}.otf"
Rails.root.join("app", "assets", "fonts", name)
end
end
def weight_hash(weight)
{
300 => "Light",
"light" => "Light",
400 => "Regular",
"regular" => "Regular",
500 => "Medium",
"medium" => "Medium",
700 => "Bold",
"bold" => "Bold",
900 => "Black",
"black" => "Black"
}[weight]
end
end
A next logical step could be to extract functionality like text generation into their helpers or classes so we can extend the template or make other templates without the need to duplicate the logic.
Please don't hesitate to explore that direction if you intend to add more templates or make an application that needs extensive use of image generation.
Further reading
Mesh Gradient Avatars in RailsOG image using Ferrum
If you haven't heard about Ferrum, it exposes a high-level API to control Chrome. It runs in headless mode by default but we can configure it to run in headful mode if we need.
As it connects to the browser via the CDP protocol, there's no need for dependencies like Selenium so it provides a better experience.
The process to generate an Open Graph image using Ferrum is the following:
- We create a view that contains the design for our OG image. That view is just like any
html.erb
view and can receive the article instance as a variable. - We perform a visit to that view using Ferrum, screenshot the view and then save the result as an image.
- We attach that image to the model so we can use it in the view.
Assuming we already have Chrome or Chromium installed, let's start the process by adding the Ferrum gem to our project:
bundle add ferrum && bundle install
Next, let's add an ArticlesController
where we will have a show
action where we will put the tags later on, and an og_image
action that we will use to take the screenshot for the image.
# config/routes.rb
get "/articles/:id", to: "articles#show", as: :article
get "/articles/:id/og-image", to: "articles#og_image", as: :article_og_image
We can now define the controller with the show
and og_image
actions:
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
end
end
Then, we add the views under app/views/articles
. Starting with the og_image.html.erb
view with the code to generate our desired image:
<div class="w-[1200px] h-[630px] bg-gradient-to-br from-[#53BDFF] to-[#2E8CF0] relative">
<%= image_tag "bit-noise.png", class: "absolute inset-0 w-full h-full object-cover mix-blend-overlay pointer-events-none select-none" %>
<div class="p-[80px] relative z-10 h-full flex flex-col">
<!-- Top: logo -->
<div>
<%= image_tag "logo-white.png", class: "w-[96px]" %>
</div>
<!-- Middle: title -->
<div class="flex-1 flex items-center">
<h2 class="text-[64px] leading-[1.1] font-black text-white max-w-[720px]"><%= @article.title %></h2>
</div>
<!-- Bottom: author left, domain right -->
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<%= image_tag @article.author_avatar_url, class: "w-12 h-12 rounded-full" %>
<span class="text-white font-medium text-[24px]"><%= @article.author_name %></span>
</div>
<div>
<span class="text-[#FFFFAA] text-[32px] font-medium">avohq.io</span>
</div>
</div>
</div>
</div>
Now, if we visit /articles/1/og_image
we get the following result:

As you can see, the result is pretty similar to what we had before but it took us a fraction of the time because the layout is simpler to resolve using HTML and Tailwind.
Let's crack the console open and generate a screenshot using Ferrum to see how it looks. We will open the browser using the width and height for the protocol:
article = Article.first
browser = Ferrum::Browser.new(timeout: 15)
browser.resize(width: 1200, height: 630)
browser.go_to("http://localhost:3000/articles/#{article.id}/og-image")
browser.screenshot(path: "og_image_#{article.id}.png")
browser.quit
Notice that we're setting the timeout
to 15 seconds, mainly because we're loading the avatar from a third-party site which might take a bit to return the avatar.
The next step is to resize the browser to take the width and height of our desired image.
After running this command, we get the following result:

The result is great and it only took us a fraction of the time, mostly because we did the layout using HTML and CSS which are more familiar to us than using ImageMagick.
Now, we need to replicate what we did before and create a job so we can automate this process when creating or updating an article.
Let's start by adding the ability to access URL helpers from within jobs:
# app/jobs/application_job.rb
class ApplicationJob < ActiveJob::Base
include Rails.application.routes.url_helpers
private
def default_url_options
Rails.application.config.action_mailer.default_url_options || {}
end
end
Then, let's add the code to make a screenshot in a OgImageFerrumJob
to distinguish it from the job for the Minimagick method:
class OgImageFerrumJob < ApplicationJob
queue_as :default
def perform(article_id)
article = Article.find(article_id)
url = article_og_image_url(article)
path = Rails.root.join("tmp/og_image_#{article.id}.png")
browser = Ferrum::Browser.new(timeout: 15)
browser.resize(width: 1200, height: 630)
browser.go_to(url)
browser.screenshot(path: path)
browser.quit
attach_to_record(article, path)
delete_tmp_image(path)
end
private
def attach_to_record(article, path)
filename = File.basename(path)
content_type = Marcel::MimeType.for(Pathname.new(path))
File.open(path) do |file|
article.og_image.attach(
io: file,
filename: filename,
content_type: content_type
)
end
end
def delete_tmp_image(path)
begin
File.delete(path) if path && File.exist?(path)
rescue => e
Rails.logger.warn("Failed to delete tmp OG image #{path}: #{e.class}: #{e.message}")
end
end
end
Notice that after generating the screenshot, we're attaching it to the article's og_image
attachment with Active Storage and then removing the file that we're temporarily storing in the tmp
folder.
The next step is to invoke the job when an article is created or updated:
class Article < ApplicationRecord
# Rest of the code
after_create_commit :create_og_image
after_update_commit :create_og_image, if: :saved_change_to_title?
# Rest of the code
private
def create_og_image
OgImageFerrumJob.perform_later(self.id)
end
end
Now, let's temporarily add the og_image
to the article's show page just to see that everything is working correctly:

Then, let's change the title to make sure that everything's working:

Now everything is working as expected. Let's finish this by adding the Open Graph tags using the meta-tags
gem:
Adding the Open Graph tags
We could add the OG tags manually using a partial but let's use the meta-tags gem which can help us further down the road.
Let's start by adding the gem and installing it:
bundle add meta-tags && bundle install
Then, we run the command to add an initializer in case we want to change any of the defaults:
bin/rails generate meta_tags:install
The next step is to display the meta tags in our application layout:
# app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<!-- Rest of the code -->
<%= display_meta_tags site: "AvoOG" %>
</head>
</html>
Now, we can add the tags in the ArticlesController
:
class ArticlesController < ApplicationController
before_action :set_article, only: [:show, :og_image]
def show
set_meta_tags(
title: "#{@article.title} - AvoOG",
description: @article.excerpt,
site: false,
og: {
title: "#{@article.title} - AvoOG",
description: @article.excerpt,
site_name: :site,
image: url_for(@article.og_image)
}
)
end
# Rest of the code
end
And this should produce the desired result:

Summary
The Open Graph protocol allows us to control how our publications look when shared on social media.
The og:image
tag is probably the most important because if we provide it with an image that looks impressive and inspires users to click it can lead to more qualified traffic to our site.
In this article we learned how to generate an OG image using Ruby with MiniMagick and Ferrum: two different approaches that have their pros and cons depending on what our goals are.
For each step we also learned how to generate them automatically when creating or updating resources in Rails so we don't have to worry too much about it for every individual post.
If your needs for Open Graph image generation are important, you can extend what we learned in this tutorial and make more templates and variations.
I hope you enjoyed this article and that it can be useful for you when implementing the feature in your applications.
Have a nice one and, happy coding!