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 the idioms you already use carry over.
One thing to know up front: OpenGraph+ never sends conditional requests. Your app will not see If-None-Match from it and will never serve it a 304. After an image’s TTL expires, OpenGraph+ fetches the page in full and compares the response’s ETag to the one it stored; a match keeps the existing image and skips the re-render. ETags save render work, not the fetch.
This page covers the Rails side. For how OpenGraph+ handles caching at the HTTP level, see the HTTP Caching guide.
ETags with fresh_when
The fresh_when helper sets an ETag header based on your record. OpenGraph+ stores it with the rendered image, and after expiry re-renders only if the ETag changed.
class PostsController < ApplicationController
def show
@post = Post.find(params[:id])
fresh_when @post
end
end
fresh_when also sets Last-Modified from updated_at; browsers and CDNs use it, but OpenGraph+ only reads the ETag.
Collection ETags
For index pages, pass a collection:
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 produces a new image on the next fetch.
Custom ETags
Build your own ETag from multiple values:
def show
@post = Post.find(params[:id])
fresh_when etag: [@post, @post.comments.maximum(:updated_at)]
end
Cache-Control with expires_in
Set explicit TTLs with expires_in:
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
# 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
TTLs shorter than your site’s minimum cache duration (default 1 hour) are floored at the minimum, so there’s no way to force a re-render on every fetch with a tiny max-age.
Combining ETags and TTLs
Use both: the TTL decides when OpenGraph+ looks again, the ETag decides whether looking again costs a render.
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 fetches the page; if the post hasn’t changed, the ETag matches and the existing image is kept without re-rendering.
stale? for conditional rendering
stale? short-circuits work when a request’s If-None-Match matches:
def show
@post = Post.find(params[:id])
if stale?(@post)
@related_posts = @post.related_posts
@comments = @post.comments.recent
end
end
Since OpenGraph+ doesn’t send conditional requests, this block always runs for its fetches. Keep stale? for browsers and CDNs; for OpenGraph+ its value is the ETag header it sets, same as fresh_when.
Minimum cache duration
Your site also has a Minimum cache duration setting in the dashboard (default 1 hour). It’s a floor, not an override: images are cached for the longer of your headers’ TTL and the minimum. Raise it when an origin sends bad or missing cache headers; set real headers in your Rails app to cache longer than the minimum.
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 |
| Frequently edited pages | Rely on the 1-hour minimum + fresh_when so edits show on the next fetch |