Overview
The Open Graph Protocol defines a standard for web pages to represent themselves as rich objects in social graphs. Created by Facebook in 2010, the protocol extends HTML meta tags to provide structured metadata that social media platforms, messaging applications, and web crawlers use to generate previews of shared links.
When a URL is shared on platforms like Facebook, Twitter, LinkedIn, or messaging applications, these services fetch the page's HTML and parse Open Graph tags to extract the title, description, image, and other metadata. This metadata controls how the shared link appears in feeds, messages, and previews, replacing default browser behaviors that might show generic or poorly formatted information.
The protocol addresses a fundamental problem in web sharing: without standardized metadata, social platforms must guess at appropriate preview content, often extracting the first image found on a page, using the HTML title tag, or showing no preview at all. Open Graph tags provide explicit instructions, giving content creators control over their social media presentation.
<meta property="og:title" content="Ruby Performance Optimization Guide" />
<meta property="og:description" content="Advanced techniques for optimizing Ruby application performance" />
<meta property="og:image" content="https://example.com/ruby-guide.jpg" />
<meta property="og:url" content="https://example.com/ruby-guide" />
The protocol has become ubiquitous across the web, implemented by content management systems, web frameworks, and static site generators. Social media platforms beyond Facebook have adopted or extended the protocol, making it a de facto standard for social sharing metadata.
Key Principles
Open Graph operates through meta tags placed in the HTML head section. Each tag uses the property attribute with an og: namespace prefix to distinguish Open Graph tags from standard HTML meta tags. The content attribute contains the actual metadata value.
The protocol defines four required properties that every Open Graph-enabled page should include:
og:title specifies the human-readable title as it should appear in social shares. This differs from the HTML title tag in that it's optimized for social context rather than browser tabs or search results. The title should be concise and descriptive without site names or additional branding.
og:type categorizes the content using a vocabulary of predefined types. The most common type is website for general pages, but the protocol defines specialized types like article, video.movie, music.song, and book that unlock additional type-specific properties.
og:image provides the URL of an image to represent the content. Social platforms download this image and display it in link previews. The image should have specific dimensions and aspect ratios for optimal display across different platforms.
og:url defines the canonical URL for the content. This URL becomes the permanent identifier in the social graph, and social platforms attribute all shares and interactions to this URL regardless of query parameters or tracking codes in the actual shared URL.
<head>
<meta property="og:title" content="Understanding Database Indexing" />
<meta property="og:type" content="article" />
<meta property="og:image" content="https://example.com/images/db-index.png" />
<meta property="og:url" content="https://example.com/articles/database-indexing" />
</head>
Beyond required properties, the protocol defines numerous optional properties. The og:description property provides a brief summary of the content, typically 2-4 sentences. The og:site_name identifies the larger site or brand, appearing separately from the content title.
Structured properties use dot notation to express relationships. An og:image property can have related properties like og:image:width, og:image:height, and og:image:alt that provide additional details about the image. Array properties allow multiple values by repeating the same property name with different content values.
<meta property="og:image" content="https://example.com/image1.jpg" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image" content="https://example.com/image2.jpg" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
Type-specific properties extend the base protocol for particular content types. The article type adds properties like article:published_time, article:author, article:section, and article:tag. Video content types include properties for video URLs, dimensions, and MIME types. Music types define properties for duration, album, and artist information.
The protocol specifies how platforms should handle missing or invalid properties. If required properties are missing, platforms fall back to HTML meta tags or heuristic content extraction. Invalid image URLs or dimensions result in platforms attempting to find alternative images on the page.
Ruby Implementation
Ruby web applications implement Open Graph tags through view templates that generate the appropriate meta tags. Rails applications typically use helper methods in layouts or partials to generate these tags dynamically based on controller instance variables or model data.
# app/helpers/open_graph_helper.rb
module OpenGraphHelper
def og_tags(title:, description:, image:, url:, type: 'website')
content_tag(:meta, nil, property: 'og:title', content: title) +
content_tag(:meta, nil, property: 'og:description', content: description) +
content_tag(:meta, nil, property: 'og:image', content: image) +
content_tag(:meta, nil, property: 'og:url', content: url) +
content_tag(:meta, nil, property: 'og:type', content: type)
end
end
The helper method generates the required Open Graph meta tags. The view template calls this helper with content-specific data, typically derived from an Active Record model or presenter object.
<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
<head>
<title><%= content_for?(:title) ? yield(:title) : 'Default Title' %></title>
<%= yield(:og_tags) if content_for?(:og_tags) %>
</head>
<body>
<%= yield %>
</body>
</html>
<!-- app/views/articles/show.html.erb -->
<% content_for :og_tags do %>
<%= og_tags(
title: @article.title,
description: @article.summary,
image: url_for(@article.cover_image),
url: article_url(@article),
type: 'article'
) %>
<% end %>
Rails provides the content_for mechanism to inject content into layout sections. This pattern keeps Open Graph logic in views where it belongs while maintaining layout flexibility.
For more sophisticated implementations, a dedicated decorator or presenter object encapsulates Open Graph metadata generation:
# app/presenters/article_og_presenter.rb
class ArticleOgPresenter
def initialize(article, view_context)
@article = article
@view = view_context
end
def tags
{
'og:title' => @article.title,
'og:description' => truncated_description,
'og:image' => image_url,
'og:image:width' => '1200',
'og:image:height' => '630',
'og:url' => canonical_url,
'og:type' => 'article',
'og:site_name' => 'TechBlog',
'article:published_time' => @article.published_at.iso8601,
'article:author' => @article.author.name,
'article:section' => @article.category.name
}
end
private
def truncated_description
@article.summary.truncate(200, separator: ' ')
end
def image_url
@view.url_for(@article.cover_image.variant(resize_to_limit: [1200, 630]))
end
def canonical_url
@view.article_url(@article, host: @view.request.host)
end
end
The presenter centralizes all Open Graph logic for articles, handling text truncation, image transformation, and URL generation. The controller instantiates the presenter and makes it available to the view:
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
@og_presenter = ArticleOgPresenter.new(@article, view_context)
end
end
Several Ruby gems provide Open Graph functionality. The meta-tags gem offers a DSL for managing all types of meta tags including Open Graph:
# Gemfile
gem 'meta-tags'
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
set_meta_tags(
title: @article.title,
description: @article.summary,
og: {
title: @article.title,
description: @article.summary,
type: 'article',
url: article_url(@article),
image: url_for(@article.cover_image)
}
)
end
end
The gem handles tag rendering, duplicate prevention, and proper escaping. The layout includes a single call to render all configured meta tags:
<!-- app/views/layouts/application.html.erb -->
<head>
<%= display_meta_tags site: 'TechBlog' %>
</head>
For API-based applications or single-page applications, server-side rendering or pre-rendering ensures social crawlers receive Open Graph tags. Ruby frameworks like Sinatra serve pre-rendered HTML for crawler user agents:
# app.rb
require 'sinatra'
require 'nokogiri'
get '/articles/:id' do
article = Article.find(params[:id])
if crawler_request?
render_with_og_tags(article)
else
send_file 'public/index.html'
end
end
def crawler_request?
user_agent = request.user_agent.to_s.downcase
user_agent.include?('facebookexternalhit') ||
user_agent.include?('twitterbot') ||
user_agent.include?('linkedinbot')
end
def render_with_og_tags(article)
template = File.read('public/index.html')
doc = Nokogiri::HTML(template)
head = doc.at_css('head')
og_tags = [
['og:title', article.title],
['og:description', article.summary],
['og:image', "https://example.com#{article.image_url}"],
['og:url', "https://example.com/articles/#{article.id}"]
]
og_tags.each do |property, content|
meta = Nokogiri::XML::Node.new('meta', doc)
meta['property'] = property
meta['content'] = content
head.add_child(meta)
end
doc.to_html
end
Practical Examples
Article pages require comprehensive Open Graph implementation to control how content appears across social platforms. The implementation includes all article-specific properties:
# app/models/article.rb
class Article < ApplicationRecord
belongs_to :author
belongs_to :category
has_one_attached :cover_image
def og_metadata
{
type: 'article',
title: title,
description: sanitized_summary,
url: canonical_url,
image: cover_image_url,
image_width: 1200,
image_height: 630,
site_name: 'TechInsights',
article: {
published_time: published_at.iso8601,
modified_time: updated_at.iso8601,
author: author.profile_url,
section: category.name,
tag: tags.pluck(:name)
}
}
end
private
def sanitized_summary
ActionView::Base.full_sanitizer.sanitize(summary).truncate(200)
end
def canonical_url
Rails.application.routes.url_helpers.article_url(
self,
host: ENV['CANONICAL_HOST']
)
end
def cover_image_url
return default_image_url unless cover_image.attached?
Rails.application.routes.url_helpers.url_for(
cover_image.variant(resize_to_fill: [1200, 630])
)
end
def default_image_url
"#{ENV['CANONICAL_HOST']}/images/default-og.jpg"
end
end
E-commerce product pages need product-specific Open Graph properties to display pricing, availability, and multiple product images:
# app/models/product.rb
class Product < ApplicationRecord
has_many_attached :images
def og_metadata
base_tags = {
type: 'product',
title: "#{name} - #{brand}",
description: short_description,
url: product_url,
image: primary_image_urls,
site_name: 'TechStore'
}
product_tags = {
'product:price:amount' => price.to_f,
'product:price:currency' => 'USD',
'product:availability' => availability_status,
'product:condition' => 'new',
'product:retailer_item_id' => sku
}
base_tags.merge(product_tags)
end
private
def primary_image_urls
images.first(4).map do |image|
Rails.application.routes.url_helpers.url_for(
image.variant(resize_to_fill: [1200, 630])
)
end
end
def availability_status
return 'out of stock' if stock_quantity.zero?
return 'preorder' if available_at > Time.current
'in stock'
end
def short_description
ActionView::Base.full_sanitizer.sanitize(description).truncate(150)
end
def product_url
Rails.application.routes.url_helpers.product_url(
self,
host: ENV['CANONICAL_HOST']
)
end
end
Video content requires video-specific properties including video file URLs, dimensions, and thumbnails:
# app/models/video.rb
class Video < ApplicationRecord
has_one_attached :video_file
has_one_attached :thumbnail
def og_metadata
{
type: 'video.other',
title: title,
description: description.truncate(200),
url: video_page_url,
image: thumbnail_url,
video: video_metadata,
site_name: 'VideoHub'
}
end
private
def video_metadata
{
url: video_file_url,
secure_url: video_file_url,
type: video_file.content_type,
width: video_width,
height: video_height,
duration: duration_seconds
}
end
def video_file_url
Rails.application.routes.url_helpers.url_for(video_file)
end
def thumbnail_url
return default_thumbnail unless thumbnail.attached?
Rails.application.routes.url_helpers.url_for(
thumbnail.variant(resize_to_fill: [1200, 630])
)
end
def video_page_url
Rails.application.routes.url_helpers.video_url(
self,
host: ENV['CANONICAL_HOST']
)
end
def default_thumbnail
"#{ENV['CANONICAL_HOST']}/images/video-default.jpg"
end
end
Dynamic locale-based content requires alternate locale tags to help platforms serve content in the appropriate language:
# app/helpers/localized_og_helper.rb
module LocalizedOgHelper
def localized_og_tags(localizable)
base_tags = og_tags_for_current_locale(localizable)
alternate_tags = alternate_locale_tags(localizable)
base_tags + alternate_tags
end
private
def og_tags_for_current_locale(localizable)
[
tag.meta(property: 'og:locale', content: current_locale_code),
tag.meta(property: 'og:title', content: localizable.title),
tag.meta(property: 'og:description', content: localizable.description)
]
end
def alternate_locale_tags(localizable)
available_locales.map do |locale|
next if locale == I18n.locale
tag.meta(
property: 'og:locale:alternate',
content: locale_code(locale)
)
end.compact
end
def current_locale_code
locale_code(I18n.locale)
end
def locale_code(locale)
{
en: 'en_US',
es: 'es_ES',
fr: 'fr_FR',
de: 'de_DE'
}[locale]
end
def available_locales
I18n.available_locales
end
end
Common Patterns
The default fallback pattern ensures pages always have valid Open Graph tags even when specific content metadata is unavailable. This pattern defines site-wide defaults that apply unless overridden:
# app/helpers/application_helper.rb
module ApplicationHelper
def default_og_tags
{
'og:title' => content_for?(:title) ? content_for(:title) : 'TechBlog',
'og:description' => 'In-depth technical articles and tutorials',
'og:image' => "#{root_url}images/default-og-image.jpg",
'og:url' => request.original_url,
'og:type' => 'website',
'og:site_name' => 'TechBlog'
}
end
def render_og_tags(custom_tags = {})
tags = default_og_tags.merge(custom_tags)
tags.map do |property, content|
tag.meta(property: property, content: content)
end.join.html_safe
end
end
The content-type dispatch pattern routes different content types to specialized Open Graph generators:
# app/services/og_tag_generator.rb
class OgTagGenerator
def self.for(object)
case object
when Article
ArticleOgGenerator.new(object)
when Product
ProductOgGenerator.new(object)
when Video
VideoOgGenerator.new(object)
when User
ProfileOgGenerator.new(object)
else
DefaultOgGenerator.new(object)
end
end
end
class ArticleOgGenerator
def initialize(article)
@article = article
end
def generate
{
'og:type' => 'article',
'og:title' => @article.title,
'og:description' => @article.summary,
'og:image' => @article.cover_image_url,
'og:url' => @article.canonical_url,
'article:published_time' => @article.published_at.iso8601,
'article:author' => @article.author.name
}
end
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
helper_method :og_tags_for
def og_tags_for(object)
OgTagGenerator.for(object).generate
end
end
The image optimization pattern ensures images meet platform requirements for dimensions, file size, and format:
# app/models/concerns/og_image_processor.rb
module OgImageProcessor
extend ActiveSupport::Concern
OG_IMAGE_WIDTH = 1200
OG_IMAGE_HEIGHT = 630
OG_IMAGE_ASPECT_RATIO = OG_IMAGE_WIDTH.to_f / OG_IMAGE_HEIGHT
def og_image_url
return default_og_image unless main_image.attached?
if main_image.metadata[:width] && main_image.metadata[:height]
process_existing_image
else
analyze_and_process_image
end
end
private
def process_existing_image
width = main_image.metadata[:width]
height = main_image.metadata[:height]
aspect_ratio = width.to_f / height
if aspect_ratio > OG_IMAGE_ASPECT_RATIO
# Image is wider than target, crop width
url_for(main_image.variant(
resize_to_fill: [OG_IMAGE_WIDTH, OG_IMAGE_HEIGHT, crop: :centre]
))
else
# Image is taller than target, crop height
url_for(main_image.variant(
resize_to_fit: [OG_IMAGE_WIDTH, OG_IMAGE_HEIGHT]
))
end
end
def analyze_and_process_image
main_image.analyze unless main_image.analyzed?
process_existing_image
end
def default_og_image
"#{ENV['CANONICAL_HOST']}/images/og-default.jpg"
end
end
The validation pattern checks Open Graph tags before page rendering to catch missing or malformed metadata:
# app/validators/og_metadata_validator.rb
class OgMetadataValidator
REQUIRED_PROPERTIES = %w[og:title og:type og:image og:url].freeze
def initialize(metadata)
@metadata = metadata
@errors = []
end
def valid?
validate_required_properties
validate_image_url
validate_url_format
validate_description_length
@errors.empty?
end
def errors
@errors
end
private
def validate_required_properties
REQUIRED_PROPERTIES.each do |property|
unless @metadata[property].present?
@errors << "Missing required property: #{property}"
end
end
end
def validate_image_url
image_url = @metadata['og:image']
return unless image_url.present?
unless image_url.start_with?('http://', 'https://')
@errors << "Image URL must be absolute: #{image_url}"
end
end
def validate_url_format
url = @metadata['og:url']
return unless url.present?
uri = URI.parse(url)
unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
@errors << "URL must use HTTP or HTTPS protocol: #{url}"
end
rescue URI::InvalidURIError
@errors << "Invalid URL format: #{url}"
end
def validate_description_length
description = @metadata['og:description']
return unless description.present?
if description.length > 300
@errors << "Description exceeds recommended length of 300 characters"
end
end
end
# Usage in controller or service
metadata = article.og_metadata
validator = OgMetadataValidator.new(metadata)
unless validator.valid?
Rails.logger.warn "OG metadata validation failed: #{validator.errors.join(', ')}"
end
The caching pattern stores generated Open Graph HTML fragments to reduce processing overhead:
# app/models/article.rb
class Article < ApplicationRecord
after_save :clear_og_cache
def cached_og_tags
Rails.cache.fetch(['og_tags', cache_key_with_version]) do
generate_og_tags
end
end
private
def generate_og_tags
metadata = og_metadata
metadata.map do |property, content|
case content
when Array
content.map { |c| meta_tag(property, c) }.join
when Hash
content.map { |key, value| meta_tag("#{property}:#{key}", value) }.join
else
meta_tag(property, content)
end
end.join
end
def meta_tag(property, content)
"<meta property=\"#{property}\" content=\"#{ERB::Util.html_escape(content)}\" />\n"
end
def clear_og_cache
Rails.cache.delete(['og_tags', cache_key_with_version])
end
end
Security Implications
Open Graph tags can expose applications to cross-site scripting attacks when user-generated content appears in tag values without proper sanitization. An attacker might inject malicious JavaScript through article titles, descriptions, or other fields that populate Open Graph tags.
# Vulnerable implementation
def og_tags
"<meta property='og:title' content='#{@article.title}' />"
end
# If @article.title contains: Hello</title><script>alert('XSS')</script>
# The generated HTML becomes:
# <meta property='og:title' content='Hello</title><script>alert('XSS')</script>' />
Rails automatically escapes content in ERB templates, but manual string concatenation bypasses this protection. Always use Rails helpers or explicit escaping:
# Secure implementation
def og_tags
tag.meta(property: 'og:title', content: @article.title)
end
# Or with explicit escaping
def og_tags
"<meta property='og:title' content='#{ERB::Util.html_escape(@article.title)}' />".html_safe
end
Content injection in Open Graph descriptions poses particular risk because descriptions often display user-generated content. Attackers can inject HTML entities, script tags, or event handlers:
# app/models/article.rb
class Article < ApplicationRecord
def safe_og_description
# Remove all HTML tags
sanitized = ActionView::Base.full_sanitizer.sanitize(description)
# Truncate to reasonable length
truncated = sanitized.truncate(200, separator: ' ')
# Encode HTML entities
ERB::Util.html_escape(truncated)
end
end
Image URLs in Open Graph tags require validation to prevent attackers from specifying malicious URLs or exposing internal resources:
# app/validators/og_image_validator.rb
class OgImageValidator
ALLOWED_SCHEMES = %w[http https].freeze
BLOCKED_HOSTS = %w[localhost 127.0.0.1 169.254.169.254].freeze
def self.valid?(url)
return false if url.blank?
uri = URI.parse(url)
# Check protocol
return false unless ALLOWED_SCHEMES.include?(uri.scheme)
# Block internal/private IPs
return false if BLOCKED_HOSTS.include?(uri.host)
return false if uri.host =~ /^10\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.|^192\.168\./
true
rescue URI::InvalidURIError
false
end
end
# Usage
def og_image_url
url = @article.cover_image_url
OgImageValidator.valid?(url) ? url : default_image_url
end
Server-side request forgery vulnerabilities can occur when applications fetch remote images to validate or process them for Open Graph tags:
# Vulnerable: Fetches any URL provided
def validate_image_dimensions(url)
response = Net::HTTP.get_response(URI(url))
# Process image...
end
# Secure: Validates URL before fetching
def validate_image_dimensions(url)
return false unless OgImageValidator.valid?(url)
uri = URI(url)
# Use explicit timeout
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https',
open_timeout: 5, read_timeout: 5) do |http|
http.get(uri.path)
end
return false unless response.is_a?(Net::HTTPSuccess)
# Validate content type
content_type = response['content-type']
return false unless content_type.start_with?('image/')
# Process image
process_image(response.body)
rescue StandardError => e
Rails.logger.error "Image validation failed: #{e.message}"
false
end
Information disclosure through Open Graph tags can reveal sensitive data when tags expose unpublished content, private information, or internal identifiers:
# app/models/article.rb
class Article < ApplicationRecord
def og_metadata
return nil unless published?
return nil if private? && !accessible_to?(current_user)
{
'og:title' => title,
'og:description' => public_summary, # Not full content
'og:url' => public_url, # Not admin URL
'og:image' => public_image_url # Not internal path
}
end
private
def public_summary
# Return sanitized, truncated summary
# Never return full content or private notes
safe_og_description
end
def public_url
# Return canonical public URL
# Never return URLs with tokens or session identifiers
Rails.application.routes.url_helpers.article_url(
self,
host: ENV['PUBLIC_HOST'],
protocol: 'https'
)
end
def public_image_url
# Return CDN URL, not application URL
# Prevents exposure of file storage structure
cover_image.cdn_url
end
end
Content Security Policy headers can conflict with Open Graph meta tags when policies restrict image sources or scripts. Configure CSP to allow Open Graph functionality:
# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
policy.default_src :self
# Allow Open Graph images from CDN
policy.img_src :self, 'https://cdn.example.com', 'data:'
# Allow social media platform scrapers (they don't execute scripts)
# But maintain strict CSP for browsers
policy.script_src :self
end
# Don't apply CSP to social crawler requests
Rails.application.config.content_security_policy_nonce_generator =
->(request) { request.user_agent&.match?(/bot|crawler/i) ? nil : SecureRandom.base64(16) }
Tools & Ecosystem
The meta-tags gem provides comprehensive meta tag management including Open Graph, Twitter Cards, and standard HTML meta tags:
# Gemfile
gem 'meta-tags'
# config/initializers/meta_tags.rb
MetaTags.configure do |config|
config.title_limit = 70
config.description_limit = 300
config.keywords_limit = 255
config.keywords_separator = ', '
end
# app/controllers/articles_controller.rb
def show
@article = Article.find(params[:id])
set_meta_tags(
title: @article.title,
description: @article.summary,
keywords: @article.tags.pluck(:name),
og: {
title: @article.title,
type: 'article',
url: article_url(@article),
image: @article.og_image_url,
description: @article.summary,
site_name: 'TechBlog'
},
article: {
published_time: @article.published_at.iso8601,
author: @article.author.name
}
)
end
Facebook's Sharing Debugger validates Open Graph implementation and shows how URLs appear when shared. The tool clears Facebook's cache and refetches metadata:
# app/services/fb_debug_service.rb
class FbDebugService
GRAPH_API_URL = 'https://graph.facebook.com/v18.0/'
def initialize(access_token)
@access_token = access_token
end
def debug_url(url)
response = HTTParty.post(
"#{GRAPH_API_URL}",
body: {
id: url,
scrape: true,
access_token: @access_token
}
)
JSON.parse(response.body)
end
def clear_cache(url)
response = HTTParty.post(
"#{GRAPH_API_URL}",
body: {
id: url,
scrape: true,
access_token: @access_token
}
)
response.success?
end
end
The LinkedIn Post Inspector examines how content appears on LinkedIn and validates Open Graph tags:
# Rake task to validate OG tags before deployment
# lib/tasks/og_validation.rake
namespace :og do
desc 'Validate Open Graph tags for published articles'
task validate: :environment do
articles = Article.published.limit(10)
articles.each do |article|
url = Rails.application.routes.url_helpers.article_url(
article,
host: ENV['PRODUCTION_HOST']
)
puts "Validating: #{url}"
validator = OgMetadataValidator.new(article.og_metadata)
if validator.valid?
puts " ✓ Valid"
else
puts " ✗ Errors:"
validator.errors.each { |error| puts " - #{error}" }
end
end
end
end
The rack-og-preview middleware serves preview pages showing how Open Graph tags will appear:
# lib/middleware/og_preview.rb
class OgPreview
def initialize(app)
@app = app
end
def call(env)
request = Rack::Request.new(env)
if request.path == '/og-preview'
return preview_response(request.params['url'])
end
@app.call(env)
end
private
def preview_response(url)
return [400, {}, ['Missing url parameter']] unless url
html = fetch_og_tags(url)
[
200,
{ 'Content-Type' => 'text/html' },
[preview_html(html)]
]
end
def fetch_og_tags(url)
# Fetch and parse Open Graph tags from URL
# Return formatted preview
end
def preview_html(og_html)
# Generate preview HTML showing how tags appear
end
end
# config/application.rb
config.middleware.use OgPreview
Automated testing tools validate Open Graph implementation across the site:
# spec/support/og_helpers.rb
module OgHelpers
def og_tag(property)
page.find("meta[property='#{property}']", visible: false)['content']
rescue Capybara::ElementNotFound
nil
end
def expect_valid_og_tags
expect(og_tag('og:title')).to be_present
expect(og_tag('og:type')).to be_present
expect(og_tag('og:image')).to be_present
expect(og_tag('og:url')).to be_present
end
end
# spec/features/article_og_spec.rb
require 'rails_helper'
RSpec.describe 'Article Open Graph tags', type: :feature do
let(:article) { create(:article, :published) }
before do
visit article_path(article)
end
it 'includes required Open Graph tags' do
expect_valid_og_tags
end
it 'sets correct article type' do
expect(og_tag('og:type')).to eq('article')
end
it 'includes article-specific properties' do
expect(og_tag('article:published_time')).to be_present
expect(og_tag('article:author')).to eq(article.author.name)
end
it 'uses optimized image dimensions' do
image_url = og_tag('og:image')
expect(image_url).to include('1200x630')
end
end
Reference
Required Properties
| Property | Description | Example |
|---|---|---|
| og:title | Title of content as it should appear in social shares | Understanding Ruby Metaprogramming |
| og:type | Content type from OG vocabulary | article |
| og:image | URL of image representing the content | https://example.com/image.jpg |
| og:url | Canonical URL for the content | https://example.com/article/123 |
Common Optional Properties
| Property | Description | Recommended For |
|---|---|---|
| og:description | Brief summary of content (2-4 sentences) | All content types |
| og:site_name | Name of the overall site | All pages |
| og:locale | Language and territory of content | Internationalized sites |
| og:locale:alternate | Available alternate locales | Multi-language content |
Image Properties
| Property | Description | Recommended Value |
|---|---|---|
| og:image:url | Same as og:image | Required |
| og:image:secure_url | HTTPS version of image URL | HTTPS sites |
| og:image:type | MIME type of image | image/jpeg, image/png |
| og:image:width | Width in pixels | 1200 |
| og:image:height | Height in pixels | 630 |
| og:image:alt | Accessible description of image | Descriptive text |
Content Types
| Type | Usage | Additional Properties |
|---|---|---|
| website | General pages and homepages | None required |
| article | Blog posts and articles | article:published_time, article:author, article:section |
| profile | Personal profiles | profile:first_name, profile:last_name, profile:username |
| video.movie | Movies and video content | video:actor, video:director, video:duration |
| music.song | Audio content | music:duration, music:album, music:musician |
| book | Books and publications | book:author, book:isbn, book:release_date |
| product | E-commerce products | product:price:amount, product:availability |
Article Properties
| Property | Description | Format |
|---|---|---|
| article:published_time | Publication timestamp | ISO 8601 |
| article:modified_time | Last modification timestamp | ISO 8601 |
| article:expiration_time | Content expiration timestamp | ISO 8601 |
| article:author | Author profile URLs | Array of URLs |
| article:section | Content category or section | String |
| article:tag | Content tags | Array of strings |
Product Properties
| Property | Description | Example |
|---|---|---|
| product:price:amount | Product price | 29.99 |
| product:price:currency | Currency code | USD |
| product:availability | Stock status | in stock |
| product:condition | Product condition | new |
| product:retailer_item_id | Internal product identifier | SKU-12345 |
Image Dimension Guidelines
| Platform | Minimum Size | Recommended Size | Aspect Ratio |
|---|---|---|---|
| 200x200 | 1200x630 | 1.91:1 | |
| 144x144 | 1200x675 | 16:9 or 1:1 | |
| 180x110 | 1200x627 | 1.91:1 | |
| 600x900 | 1000x1500 | 2:3 |
Rails Helper Method Reference
| Method | Purpose | Returns |
|---|---|---|
| set_meta_tags | Configure meta tags in controller | Hash |
| display_meta_tags | Render meta tags in view | HTML string |
| content_for | Define content sections | Nil |
| yield | Retrieve content sections | String |
| tag.meta | Generate meta tag | HTML string |
| url_for | Generate asset URLs | String |
Validation Checklist
| Check | Requirement | Tools |
|---|---|---|
| Required tags present | og:title, og:type, og:image, og:url | Facebook Debugger |
| Image accessible | Returns 200 status code | curl, wget |
| Image dimensions | Minimum 200x200, recommended 1200x630 | Image inspector |
| URL format | Absolute URLs with protocol | URL validator |
| Content length | Title under 70 chars, description under 300 | Character counter |
| HTML escaping | All content properly escaped | View source |
| HTTPS protocol | All URLs use HTTPS | Protocol checker |
Ruby Gem Comparison
| Gem | Features | Maintenance Status |
|---|---|---|
| meta-tags | Comprehensive meta tag DSL, SEO helpers | Active |
| social-share-button | Social sharing with OG support | Active |
| twitter-cards | Twitter Card implementation | Deprecated |
| rails-seo | SEO and social meta tags | Active |
Common User Agent Strings
| Platform | User Agent Pattern | Purpose |
|---|---|---|
| facebookexternalhit | Crawling shared links | |
| Twitterbot | Card validation | |
| LinkedInBot | Preview generation | |
| Slack | Slackbot-LinkExpanding | Link unfurling |
| Link previews |