# Rails

## Getting Started

## Create a connection

1. Sign in to [OpenGraph+](/)
2. Go to your website's **Meta Tags** page
3. Create a new connection and copy your connection URL

Your connection URL looks like `https://$OGPLUS_KEY.ogplus.net`.

## Find your layout

Rails layouts live in `app/views/layouts/`. Most apps use `application.html.erb` as the base layout. All views rendered within this layout inherit its `<head>` content.

## Add Open Graph meta tags

Add the following meta tags to your layout's `<head>`. These control how your pages appear when shared on Twitter, Slack, LinkedIn, and other platforms.

```erb
<!-- app/views/layouts/application.html.erb -->
<html>
  <head>
    <meta property="og:title" content="<%= content_for(:og_title) || "My Site" %>">
    <meta property="og:description" content="<%= content_for(:og_description) || "Welcome to my site" %>">
    <meta property="og:url" content="<%= request.original_url %>">
    <meta property="og:site_name" content="My Site">
    <meta property="og:type" content="website">
    <meta property="og:image" content="https://$OGPLUS_KEY.ogplus.net<%= request.path %>">
    <meta name="twitter:card" content="summary_large_image">
  </head>
  <body>
    <%= yield %>
  </body>
</html>
```

Replace `https://$OGPLUS_KEY.ogplus.net` with your connection URL, and `"My Site"` with your actual site name.

### What each tag does

| Tag | Purpose |
|-----|---------|
| `og:title` | The title shown in the link preview |
| `og:description` | The description shown below the title |
| `og:url` | The canonical URL of the page |
| `og:site_name` | Your website's name |
| `og:type` | Content type (`website`, `article`, `product`) |
| `og:image` | The social card image (powered by OpenGraph+) |
| `twitter:card` | Tells Twitter to show a large image card |

## Set dynamic tags from views

Use `content_for` in your views to set page-specific titles and descriptions:

```erb
<%# app/views/posts/show.html.erb %>
<% content_for(:og_title) { @post.title } %>
<% content_for(:og_description) { @post.excerpt } %>

<h1><%= @post.title %></h1>
<p><%= @post.body %></p>
```

For a product page:

```erb
<%# app/views/products/show.html.erb %>
<% content_for(:og_title) { @product.name } %>
<% content_for(:og_description) { "#{number_to_currency(@product.price)} - #{@product.description}" } %>

<h1><%= @product.name %></h1>
```

### Article type for blog posts

Set `og:type` to `article` for blog posts. Add this to your layout or override per-view:

```erb
<meta property="og:type" content="<%= content_for(:og_type) || "website" %>">
```

Then in your post view:

```erb
<% content_for(:og_type) { "article" } %>
```

## Verify

Start your development server and view the page source. Check that all `og:` meta tags are present with the correct values.

Open the preview tool in your OpenGraph+ dashboard and paste a URL from your site to see the social card image.


## Tag Helpers

The [Getting Started](/docs/rails/connect) guide puts meta tags directly in your layout. That works fine for small apps, but as your app grows you'll want to extract them into partials or helpers. Here are a few ways to do that.

## Shared partial

Pull your meta tags into a partial so you can reuse them across layouts:

```erb
<%# app/views/shared/_open_graph.html.erb %>
<meta property="og:title" content="<%= og_title || "My Site" %>">
<meta property="og:description" content="<%= og_description || "Welcome to my site" %>">
<meta property="og:url" content="<%= request.original_url %>">
<meta property="og:site_name" content="My Site">
<meta property="og:type" content="<%= og_type || "website" %>">
<meta property="og:image" content="https://$OGPLUS_KEY.ogplus.net<%= request.path %>">
<meta name="twitter:card" content="summary_large_image">
```

Render it in your layout:

```erb
<head>
  <%= render "shared/open_graph",
    og_title: content_for(:og_title),
    og_description: content_for(:og_description),
    og_type: content_for(:og_type) %>
</head>
```

## Helper method

If you want to build the tags in Ruby instead of ERB, create a helper:

```ruby
# app/helpers/open_graph_helper.rb
module OpenGraphHelper
  def og_meta_tags(title: nil, description: nil, type: "website")
    safe_join([
      tag.meta(property: "og:title", content: title || "My Site"),
      tag.meta(property: "og:description", content: description || "Welcome to my site"),
      tag.meta(property: "og:url", content: request.original_url),
      tag.meta(property: "og:site_name", content: "My Site"),
      tag.meta(property: "og:type", content: type),
      tag.meta(property: "og:image", content: "https://$OGPLUS_KEY.ogplus.net#{request.path}"),
      tag.meta(name: "twitter:card", content: "summary_large_image"),
    ])
  end
end
```

Then in your layout:

```erb
<head>
  <%= og_meta_tags title: content_for(:og_title),
                   description: content_for(:og_description),
                   type: content_for(:og_type) %>
</head>
```

## Setting tags from controllers

You can also set `content_for` in a `before_action` instead of in the view. This is useful when the controller already loads the record:

```ruby
class PostsController < ApplicationController
  before_action :set_post, only: :show

  def show
  end

  private

  def set_post
    @post = Post.find(params[:id])
    content_for(:og_title) { @post.title }
    content_for(:og_description) { @post.excerpt }
    content_for(:og_type) { "article" }
  end
end
```


## Customize

OpenGraph+ captures your page in a headless browser and renders it as an image. You control what gets captured using meta tags in your layout and CSS in your stylesheets.

All of the rendering options below are standard HTML meta tags and CSS, so they work the same regardless of framework. The [HTML, CSS, & HTTP guide](/docs/html-css) covers each one in detail. This page shows how to wire them into a Rails app.

## Meta tags

Add these to your layout's `<head>` alongside your `og:image` tag. They're all optional.

```erb
<head>
  <meta property="og:image" content="https://$OGPLUS_KEY.ogplus.net<%= request.path %>">
  <meta name="twitter:card" content="summary_large_image">

  <%# Render at 800px wide instead of the default %>
  <meta property="og:plus:viewport:width" content="800">

  <%# Only capture this element instead of the full page %>
  <meta property="og:plus:selector" content=".post-header">

  <%# Inject inline styles on the captured element %>
  <meta property="og:plus:style" content="padding: 60px; background: #0f172a; color: white;">
</head>
```

See the [Rendering](/docs/html-css/rendering) guide for what each meta tag does and how they interact.

## CSS styling

OpenGraph+ adds a `data-ogplus` attribute to your `<html>` element during capture. Use it in your stylesheets to hide navigation, adjust spacing, or restyle anything for the social card without affecting your actual site.

```css
/* app/assets/stylesheets/application.css */
html[data-ogplus] {
  nav { display: none; }
  footer { display: none; }
  .hero { padding: 60px; }
}
```

If you're using Tailwind, there's a plugin that gives you `ogplus:` variants like `ogplus:hidden` and `ogplus-twitter:bg-sky-500`. See [CSS Styling](/docs/html-css/data-attributes) for plain CSS examples and [Tailwind setup](/docs/html-css/data-attributes#tailwind-css).

## Templates

For fully custom social card layouts that pull content from your page, use `<template>` elements. These let you build a completely different layout for screenshots without touching your visible page.

```erb
<%# app/views/layouts/application.html.erb %>
<template id="ogplus">
  <div style="padding: 48px; background: #0f172a; color: white; height: 100%;">
    <h1 style="font-size: 48px;">
      ${document.querySelector('h1')?.textContent}
    </h1>
  </div>
</template>
```

See the [Templates](/docs/html-css/templates) guide for expression syntax, platform-specific templates, and full examples.

## Testing locally

The [Preview Bookmarklet](/docs/html-css/bookmarklet) sets the `data-ogplus` attribute in your browser so you can see how your CSS and Tailwind variants look without deploying or waiting for a real crawler to hit your page.

## Full example

A blog post layout with all the pieces together:

```erb
<head>
  <meta property="og:title" content="<%= content_for(:og_title) || "My Site" %>">
  <meta property="og:description" content="<%= content_for(:og_description) || "Welcome" %>">
  <meta property="og:image" content="https://$OGPLUS_KEY.ogplus.net<%= request.path %>">
  <meta name="twitter:card" content="summary_large_image">
  <meta property="og:plus:selector" content=".post-header">
  <meta property="og:plus:style" content="padding: 60px; background-color: #0f172a; color: white;">
  <meta property="og:plus:viewport:width" content="800">
</head>
```


## Caching

OpenGraph+ reads HTTP cache headers from your responses to decide when to re-render social card images. Rails has built-in support for ETags and `Cache-Control`, so this works well out of the box.

This page covers the Rails side. For how OpenGraph+ handles caching at the HTTP level, see the [HTTP Caching](/docs/html-css/caching) guide.

## ETags with fresh_when

The `fresh_when` helper sets ETag and Last-Modified headers based on your record. OpenGraph+ uses these for conditional requests. If your content hasn't changed, your server returns `304 Not Modified` and OpenGraph+ serves the cached image.

```ruby
class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id])
    fresh_when @post
  end
end
```

This sets both `ETag` (based on the record's cache key) and `Last-Modified` (based on `updated_at`).

### Collection ETags

For index pages, pass a collection:

```ruby
def index
  @posts = Post.published.order(created_at: :desc)
  fresh_when @posts
end
```

Rails generates an ETag from the entire collection, so any change to any post invalidates the cache.

### Custom ETags

Build your own ETag from multiple values:

```ruby
def show
  @post = Post.find(params[:id])
  fresh_when etag: [@post, current_user&.id, @post.comments.maximum(:updated_at)]
end
```

## Cache-Control with expires_in

Set explicit TTLs with `expires_in`:

```ruby
class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id])
    expires_in 1.day, public: true
  end
end
```

This sets `Cache-Control: public, max-age=86400`. OpenGraph+ serves the cached image for 24 hours without hitting your server.

### Common patterns

```ruby
# Static pages - cache for a week
expires_in 1.week, public: true

# Blog posts - cache for a day
expires_in 1.day, public: true

# Dynamic content - cache for an hour
expires_in 1.hour, public: true

# User-specific content - don't cache
expires_in 0, public: false
```

## Combining ETags and TTLs

Use both for fast responses during the TTL and efficient revalidation after.

```ruby
def show
  @post = Post.find(params[:id])
  expires_in 1.hour, public: true
  fresh_when @post
end
```

During the first hour, OpenGraph+ serves the cached image immediately. After that, it revalidates with the ETag. If the post hasn't changed, your server returns 304 and OpenGraph+ keeps serving the cached image.

## stale? for conditional rendering

Use `stale?` when you only want to do work if the cache is invalid:

```ruby
def show
  @post = Post.find(params[:id])
  
  if stale?(@post)
    # Only runs if the ETag doesn't match
    @related_posts = @post.related_posts
    @comments = @post.comments.recent
  end
end
```

When OpenGraph+ sends a conditional request and the ETag matches, Rails returns 304 without executing the block.

## Website-level TTL override

OpenGraph+ also has a website-level cache TTL setting in your dashboard. When set, it overrides HTTP headers entirely. This is handy when origin servers have bad or missing cache headers.

If you control your Rails app, stick with HTTP headers for caching. The website TTL override is a fallback for sites you don't control.

## Recommendations

| Page type | Strategy |
|-----------|----------|
| Static pages | `expires_in 1.week, public: true` |
| Blog posts | `expires_in 1.day` + `fresh_when @post` |
| Index pages | `expires_in 1.hour` + `fresh_when @posts` |
| User dashboards | `expires_in 0` or no caching |


## Deploy to Production

Since OpenGraph+ uses meta tags, there's nothing extra to configure for production. No API keys, no environment variables, no credentials file. If your meta tags are in your layout, they work everywhere your layout works.

## Verify after deploying

After you deploy, open your production site and view the page source. Look for your `og:image` meta tag and confirm it points to your `ogplus.net` connection URL with the correct path.

```html
<meta property="og:image" content="https://$OGPLUS_KEY.ogplus.net/your-page-path">
<meta name="twitter:card" content="summary_large_image">
```

Then paste one of your production URLs into the [OpenGraph+ preview tool](/previews/new) to confirm the image renders correctly.

## Common deployment issues

### Wrong path in og:image

If `request.path` isn't returning what you expect, check that your production server isn't rewriting paths or stripping them behind a reverse proxy.

### Page looks different in social cards

OpenGraph+ loads your page in a headless browser, so it sees whatever your production server returns. If your page requires authentication, the social card will show your login page. Make sure pages you want social cards for are publicly accessible.

### Caching

If you've updated your page but the social card still shows old content, check your cache settings. See the [Caching](/docs/rails/caching) guide for how OpenGraph+ uses HTTP cache headers from your Rails app.


## Troubleshooting

Something not working? Here are the most common issues.

## No og:image tag in page source

View your page source and search for `og:image`. If it's missing:

1. Check that the meta tag is in your layout's `<head>` section
2. Make sure the layout is being used by the controller rendering the page
3. If you're using `content_for` blocks, confirm the `yield` is inside `<head>`

## Image not rendering

If the `og:image` tag is present but the image isn't showing up when you share a link:

1. Copy the `og:image` URL from your page source and open it directly in your browser. You should see the rendered image or get redirected to one.
2. Make sure your page is publicly accessible. OpenGraph+ loads your page in a headless browser, so if it hits a login wall, that's what the social card will show.
3. Check that your connection URL is correct. It should look like `https://$OGPLUS_KEY.ogplus.net` followed by the page path.

## Wrong page content in the image

OpenGraph+ screenshots whatever your server returns at that URL. If the image shows unexpected content:

1. Open the page at the same path in your browser and check that it renders what you expect
2. If you're behind a CDN or reverse proxy, make sure it isn't rewriting the path
3. Check the [Customize](/docs/rails/customize) guide for controlling what gets captured using selectors and CSS

## Stale images

If you've updated a page but the social card still shows old content:

1. Check your HTTP cache headers. OpenGraph+ respects `Cache-Control` and `ETag` headers from your Rails app.
2. You can purge the cache for a specific page from your OpenGraph+ dashboard.
3. See the [Caching](/docs/rails/caching) guide for details on how to set up cache invalidation.

