Rails API + Open Graph: Add OG Tags to Your API-Mode App

Rails API mode doesn't render HTML — here's how to add og:title, og:image, and Twitter card tags using a separate SSR frontend or a hybrid approach with ActionView.

The core problem with Rails API mode

When you generate a Rails app with --api, it strips out ActionView and middleware needed to render HTML. Social crawlers (Twitter, LinkedIn, Slack, Discord) scrape raw HTML for <meta property="og:..."> tags. If your Rails app only returns JSON, those crawlers get nothing.

You have three options:

  1. Add OG tags in your SSR frontend (Next.js, Nuxt, etc.)
  2. Add a minimal HTML rendering layer back to Rails API mode
  3. Build a server-side OG proxy endpoint in Rails

Option 1: OG tags in Next.js frontend (recommended)

If you have a Next.js or Nuxt.js frontend consuming your Rails API, add OG tags in the frontend's generateMetadata:

// app/posts/[slug]/page.tsx (Next.js App Router)
export async function generateMetadata({ params }: { params: { slug: string } }) {
  const post = await fetch(`https://api.myapp.com/posts/${params.slug}`).then(r => r.json());

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [{ url: post.og_image_url || `https://myapp.com/og/${params.slug}.png`, width: 1200, height: 630 }],
      type: 'article',
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [post.og_image_url || `https://myapp.com/og/${params.slug}.png`],
    },
    alternates: { canonical: `https://myapp.com/posts/${params.slug}` },
  };
}

Your Rails API should return og_image_url in the JSON response so the frontend doesn't have to construct it:

# app/controllers/api/v1/posts_controller.rb
def show
  post = Post.find_by!(slug: params[:slug])
  render json: {
    title: post.title,
    excerpt: post.excerpt,
    body: post.body,
    og_image_url: rails_blob_url(post.og_image, only_path: false),
    published_at: post.published_at.iso8601,
  }
end

Option 2: Re-enable HTML rendering in Rails API mode

You can add HTML rendering back to a specific controller without leaving API mode:

# config/application.rb
# Add ActionView back for the OG proxy
config.api_only = true
# app/controllers/og_controller.rb
class OgController < ActionController::Base  # Not API::Base
  def show
    @post = Post.find_by!(slug: params[:slug])

    render html: og_html(@post).html_safe, layout: false
  end

  private

  def og_html(post)
    <<~HTML
      <!DOCTYPE html>
      <html>
      <head>
        <meta property="og:title" content="#{CGI.escapeHTML(post.title)}" />
        <meta property="og:description" content="#{CGI.escapeHTML(post.excerpt)}" />
        <meta property="og:image" content="https://myapp.com/og/#{post.slug}.png" />
        <meta property="og:url" content="https://myapp.com/posts/#{post.slug}" />
        <meta property="og:type" content="article" />
        <meta name="twitter:card" content="summary_large_image" />
        <meta name="twitter:image" content="https://myapp.com/og/#{post.slug}.png" />
      </head>
      <body></body>
      </html>
    HTML
  end
end
# config/routes.rb
get '/og/:slug', to: 'og#show'

Dynamic OG image endpoint in Rails

Serve dynamically generated OG images from Rails using the gruff or mini_magick gem:

# app/controllers/api/og_images_controller.rb
class Api::OgImagesController < ApplicationController
  def show
    post = Post.find_by!(slug: params[:slug])

    # Cache the generated image using Rails cache
    png = Rails.cache.fetch("og-image/#{post.slug}/#{post.updated_at.to_i}", expires_in: 1.day) do
      generate_og_image(post.title, post.excerpt)
    end

    send_data png,
      type: 'image/png',
      disposition: 'inline',
      filename: "#{post.slug}-og.png"
  end

  private

  def generate_og_image(title, description)
    # Using ImageMagick via MiniMagick
    image = MiniMagick::Image.create('.png') do |f|
      f.size '1200x630'
      f.background '#0f0f0f'
      f.fill 'white'
      f.font 'Helvetica-Bold'
      f.pointsize 56
      f.gravity 'NorthWest'
      f.annotate '+60+200', title[0..50]
      f.pointsize 28
      f.fill '#aaaaaa'
      f.annotate '+60+300', description[0..100]
    end
    image.to_blob
  end
end

Common Rails API OG mistakes

  • Assuming JSON gets scraped — social crawlers only read HTML. JSON responses produce no OG preview.
  • Missing CORS headers on OG image endpoint — OG image URLs are fetched directly by scrapers; they don't need CORS headers, but they do need to be publicly accessible (no auth).
  • Using Active Storage private blobs — OG images must be publicly accessible URLs. Use Active Storage's public service or sign URLs with long expiry.

Verify your Rails API OG tags

After deploying, paste your URL into OGFixer to see exactly how your app's link previews appear on Twitter, LinkedIn, Slack, and Discord — and catch missing or broken tags before sharing.