Chapter 4

Caching

Control when social card images refresh

OpenGraph+ reads HTTP cache headers from your Rails responses to decide when to re-render social card images. Rails has built-in support for ETags and cache headers, so this is pretty straightforward.

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.

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:

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:

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:

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

# 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.

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:

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