Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Generate an Open Graph image in Ruby by rendering a fixed-size HTML/CSS card, capturing it with a Chromium-based renderer, storing the resulting PNG at a stable public URL, and placing that URL in the page’s og:image metadata. This approach lets you use normal Rails view data—title, author, logo, category, and publication date—while keeping the design in HTML and CSS.

For most Rails applications, choose Grover when you want a straightforward HTML-to-image wrapper, Ferrum when you need direct Chrome control, or a hosted HTML-to-image API when you do not want to operate a browser binary. The sections below show the self-hosted implementations, deployment details, failure fixes, and a hosted alternative.

The rendering pipeline

  1. Prepare card data. Select a bounded set of fields such as title, author, brand name, and accent color. Escape user-controlled text through your normal Rails view helpers.
  2. Render a dedicated template. Build a card at a fixed viewport, commonly 1200 × 630 pixels. Keep this template separate from the article page so layout changes cannot unexpectedly alter social images.
  3. Capture the HTML. Use Grover or Ferrum to let Chromium perform layout, font loading, image decoding, and CSS rendering.
  4. Persist the bytes. Write the PNG (or JPEG/WebP) to object storage or another cache and give it a stable, publicly reachable URL.
  5. Publish metadata. Add that URL to the document head as og:image. Social crawlers must be able to fetch it without authentication.

A stable URL is preferable to generating a new image during every page request. Regenerate when the title, author, branding, or template version changes, and cache the result by a content digest or record version.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Option 1: Grover and a Rails view

Grover is a Ruby wrapper that sends a URL or inline HTML through Puppeteer and Chromium and can return PNG or JPEG bytes. It is convenient when your card already exists as a Rails template.

#1 Best Overall

Install the Ruby and browser dependencies

Add Grover to your Gemfile, bundle it, and install the Puppeteer/Chromium runtime required by the selected Grover version. Browser installation is part of deployment: your build image, worker, and local development environment all need a compatible Node/Puppeteer and Chromium setup.

# Gemfile
gem "grover"

# shell
bundle install
# Install the Puppeteer package and browser according to your deployment image.

The Grover gem registry lists version 1.2.6 dated January 14, 2026. An opened 1.2.4 page states Ruby >= 3.0.0, < 3.5.0; that requirement is version-specific, so verify the requirements of the exact version you select before upgrading Ruby or locking the gem.

Create a dedicated card template

<!-- app/views/og_cards/show.html.erb -->
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <style>
      * { box-sizing: border-box; }
      html, body { margin: 0; width: 1200px; height: 630px; }
      body {
        display: flex;
        flex-direction: column;
        justify-content: space-between;
        padding: 64px;
        color: #f7f7fb;
        background: #171827;
        font-family: Arial, sans-serif;
      }
      .eyebrow { color: #9ea7ff; font-size: 24px; font-weight: 700; }
      h1 { max-width: 1040px; margin: 24px 0; font-size:  sixty; line-height: 1.08; }
      .byline { font-size: 26px; color: #c9cbe0; }
      .brand { font-size: 24px; font-weight: 700; }
    </style>
  </head>
  <body>
    <div>
      <div class="eyebrow"><%= h(@post.category) %></div>
      <h1><%= h(@post.title) %></h1>
      <div class="byline">By <%= h(@post.author.name) %></div>
    </div>
    <div class="brand">Example Magazine</div>
  </body>
</html>

Replace the accidental placeholder value in the example CSS with a valid size such as 60px before use. The important properties are the explicit 1200 × 630 canvas and bounded text. Long titles need a tested line limit, smaller type, or an ellipsis policy so they cannot overflow.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Render and save the PNG

# app/services/open_graph_image.rb
class OpenGraphImage
  def self.generate(post)
    html = ApplicationController.render(
      template: "og_cards/show",
      assigns: { post: post }
    )

    image = Grover.new(
      html,
      display_url: "https://example.com/og-cards/#{post.id}"
    ).to_png(
      viewport: { width: 1200, height: 630 },
      full_page: false
    )

    Rails.root.join("tmp", "og-#{post.cache_key_with_version}.png").tap do |path|
      File.binwrite(path, image)
    end.to_s
  end
end

Grover’s documentation demonstrates rendering a Rails template to a string before passing it to Grover. The display_url is significant when the HTML contains relative URLs: without an appropriate base URL, Chromium may resolve relative images, stylesheets, or fonts against its default display URL. Using absolute asset URLs is another reliable solution.

Make generation asynchronous

Do not make a reader wait for Chromium unless that behavior is intentional. Enqueue a job after publishing or editing a post, write the bytes to your storage layer, and update the record with the public image URL. A cache key containing the post version and template version prevents stale cards while avoiding duplicate renders. The implementation should also clean up temporary files after upload.

Option 2: Ferrum for direct Chrome control

Ferrum is a high-level Ruby API for Chrome. It communicates through the Chrome DevTools Protocol and does not require Selenium, WebDriver, or ChromeDriver. It runs headless by default, but Chrome or Chromium remains a production dependency.

Minimal screenshot service

require "ferrum"

class CardScreenshot
  def self.capture(url, output_path)
    browser = Ferrum::Browser.new(
      browser_path: ENV["BROWSER_PATH"] # omit when Chrome is already on PATH
    )
    browser.go_to(url)
    browser.resize(width: 1200, height: 630)
    browser.screenshot(path: output_path, full: false)
  ensure
    browser&.quit
  end
end

CardScreenshot.capture(
  "https://example.com/og-cards/42",
  "tmp/og-42.png"
)

Ferrum’s documented setup allows Chrome to be found on PATH or through BROWSER_PATH. Always call quit, including on errors, or worker processes can accumulate browser instances. For a private Rails template, navigate to a route that renders only the card and protect that route with an internal authorization mechanism; for a public route, ensure the social crawler can reach it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

When Ferrum is the better fit

  • Use it when you need direct navigation, viewport, JavaScript, or browser lifecycle control.
  • Use Grover when you prefer a higher-level “HTML in, image bytes out” interface.
  • Both approaches require browser-related deployment work and should be tested in the same type of container used by production workers.

Hosted HTML-to-image rendering

A hosted Ruby client can accept HTML and return an image URL, removing the need to package and patch a local browser. The documented html2img client describes Open Graph and per-post images as use cases, demonstrates a 1200 × 630 render, requires Ruby 3.1 or newer and an API key, and states that free-tier renders are hosted for seven days while paid-plan renders are permanent. Retention and pricing are service terms, so confirm them when you integrate.

Keep the API key on the server, not in browser JavaScript. Evaluate the provider’s latency, privacy policy, retention, failure behavior, and continuity requirements before sending private or unpublished content. A hosted service changes the operational burden; the available documentation does not establish that it is faster than Grover or Ferrum.

Design and asset rules that prevent broken cards

Fonts and images

  • Wait for web fonts and images before capturing. A screenshot taken before they finish loading can fall back to a different font or show empty image boxes.
  • Prefer absolute, publicly reachable asset URLs or provide a correct base/display URL. Relative paths are a documented Grover failure point.
  • Use local assets or a controlled asset host when reproducibility matters. External services can be unavailable during a background job.

Text and accessibility

  • Escape titles, author names, and category values with the normal Rails view escaping.
  • Set explicit line-height, width, and maximum text length. Test the longest real title, not only a short sample.
  • Do not put essential information only in a tiny footer; social previews are often displayed at small sizes.

Metadata in the page

<meta property="og:image" content="https://cdn.example.com/og/posts/42-v3.png">

The image URL should remain stable long enough for crawlers to cache it, return an image content type, and work without cookies or a logged-in session. If you replace an image at the same URL, some platforms may continue showing an older cached copy; versioned filenames avoid that ambiguity.

Testing, performance, and cost decisions

Test the complete path

  1. Open the generated URL directly in a clean browser and confirm the dimensions and content type.
  2. Inspect the page source to verify the final og:image value, not merely a client-side DOM update.
  3. Test records with long titles, missing avatars, unusual Unicode characters, and slow images.
  4. Run the same job in the production-like worker image, including its fonts, Chrome binary, environment variables, and network policy.

Control rendering overhead

Reuse a browser process carefully or create one per isolated job according to your worker model; the reviewed documentation does not provide a universal throughput number. Limit concurrency to the CPU and memory available to your workers, and measure your own queue time and render duration. Cache by record and template version so a page view does not trigger a new capture.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose the operational model

Approach Control What you operate Image lifecycle
Grover HTML template through a Puppeteer/Chromium wrapper Ruby gem, Node/Puppeteer, Chromium, worker resources Your storage and public URL
Ferrum Direct Chrome DevTools Protocol control Ruby process, Chrome/Chromium binary, cleanup, workers Your storage and public URL
Hosted html2img client HTML request and provider options API key, vendor integration, privacy and continuity review Documented URL output; retention depends on plan
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting

The image is blank or partly rendered

Check that the worker can reach every asset URL, that the browser is not blocked by authentication, and that capture waits for the required selector, fonts, or images. Log the final HTML and the browser console when diagnosing a failing template.

Images or styles are missing

Replace relative paths with absolute URLs or set Grover’s display_url. Confirm that the asset host permits requests from the worker and that HTTPS certificates are valid inside the container.

Chrome cannot start

Install a compatible Chromium package, expose its binary on PATH, or set Ferrum’s BROWSER_PATH. Verify sandbox and shared-memory settings for your container rather than copying flags blindly.

Ruby or gem version conflicts

Check the exact Grover release’s Ruby requirement; requirements from an older registry page do not automatically apply to a newer release. Lock the gem and browser versions together in deployment.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Cards are stale

Include the post version and template version in the storage key or URL. Regenerate after edits and update the metadata URL when you need crawlers to fetch a new asset.

Or skip the browser setup

ScreenshotNeo provides a hosted screenshot API and MCP server. It removes cookie/consent banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP tools—take_screenshot, get_page_info, and capture_pdf—let Claude, Cursor, or another MCP client request captures without custom browser orchestration.

For a card route that your application exposes publicly, make one GET request:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/og-cards/42 -o shot.webp

The API also supports PNG, JPEG, WebP, full-page or element captures, custom CSS and JavaScript, waits, device presets, retina scale, headers, cookies, user agents, geolocation, caching TTLs, signed image links, asynchronous webhooks, and bulk capture. See the ScreenshotNeo documentation for parameter names and response details. The parameter names used by other screenshot APIs are accepted, which can simplify a migration.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

There is a free allowance of 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is included on every plan. Create a free ScreenshotNeo account when you want to avoid packaging and maintaining a browser.

Frequently Asked Questions

Should an Open Graph image be generated synchronously during a page request?

Usually no. Generate it when the post is created or changed, store it, and serve the saved URL so social crawlers and readers do not wait for a browser render.

Can I use a private Rails route as the screenshot source?

Only if the renderer can authenticate. A public, minimal card route or inline HTML avoids session and permission problems; never expose unpublished content unintentionally.

Which output format should I store?

PNG is a dependable default for text-heavy cards. JPEG or WebP can reduce bytes when their visual quality and crawler support meet your requirements; verify the actual response and platform behavior.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.