Open Graph Image Generation in Rails

By Exequiel Rozas

- October 20, 2025

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.

The standard Open Graph image size is 1200×630px which is the size of Facebook's posts. Some platforms specify a slightly different size which varies a couple of pixels but, for practical reasons, that size is fine.

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:

Open Graph image template design

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.

If you're reading this article because you've heard that custom Open Graph images might help with SEO, don't hesitate to check our Rails SEO guide to see how you can improve your results.

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:

Avo admin panel article index

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:

Image placeholder with a gradient

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:

Image Magick with image overlay

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:

Add logo to image with ImageMagick

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:

Text generation without multiline in Image Magick

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:

OG Image with multi-line text using ImageMagick

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:

Open graph image with domain name

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:

Open graph image with author information

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.

OG 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:

Open Graph image generated with ERB and Tailwind

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:

Open Graph image generated with Ferrum

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:

Open graph image displayed in the article show page

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

Open graph image with Ferrum and callbacks

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:

Open Graph tags with the auto generated image

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!


Build your next rails app 10x faster with Avo

Avo dashboard showcasing data visualizations through area charts, scatterplot, bar chart, pie charts, custom cards, and others.

Find out how Avo can help you build admin experiences with Rails faster, easier and better.