Encited

How to Integrate Pre-rendering with Ruby on Rails

How Rails pre-rendering works#

Ruby on Rails can add Encited as the first Rack middleware so crawlers receive rendered HTML before Rails routing and authentication run.

The Rack middleware sends eligible crawler GET requests to Encited. Browser visits and render fallbacks call the next Rails application in the Rack chain.

Prerequisites#

  • Access to add a Rack middleware class under app/middleware.
  • Permission to insert EncitedPrerender at position zero in the Rails middleware stack.
  • LOVABLEHTML_API_KEY configured for Puma, Passenger, or the production process manager.

Set up pre-rendering with Rails#

1. Add the Rack middleware#

Save the snippet as app/middleware/encited_prerender.rb. It only uses Ruby's standard HTTP libraries.

encited_prerender

# app/middleware/encited_prerender.rb
require "net/http"
require "uri"

class EncitedPrerender
  CRAWLER = /(?:(OAI-SearchBot|ChatGPT-User(?:\/\d+\.\d+)?|GPTBot|ChatGPT|OpenAI-))|(?:(anthropic-ai|ClaudeBot|claude-web|Claude-Web|Claude-User|Claude-SearchBot))|(?:\b(GrokBot|xAI-Grok|xAI-Bot|xAI-SearchBot)\b)|(?:(PerplexityBot|Perplexity-User))|(?:(Google-InspectionTool|GoogleOther(?:-Image|-Video)?|Googlebot|Google-CloudVertexBot|Storebot-Google|Google-NotebookLM|GoogleAgent-URLContext|GoogleAIOverviewRenderer|Google-Lens|Google-Jetski-Antigravity))|(?:(Microsoft-Copilot|CopilotBot|Copilot-Chat|Microsoft-CopilotPlugin))|(?:(BingBot|bingbot|BingPreview))|(?:facebookexternalhit.*Twitterbot|Twitterbot.*facebookexternalhit)|(?:(Meta-ExternalAgent|meta-externalagent|meta-externalfetcher|MetaBot|FacebookBot|facebookexternalhit))|(?:LinkedInBot)|(?:Amazonbot)|(?:(Applebot-Extended|Applebot|AppleNewsBot))|(?:Bytespider)|(?:(DuckAssistBot|DuckDuckBot))|(?:(cohere-training-data-crawler|cohere-ai|CohereAI))|(?:MistralAI-User)|(?:AI2Bot)|(?:CCBot)|(?:Diffbot)|(?:omgili)|(?:TimpiBot)|(?:YouBot)|(?:ExaSearchBot)|(?:(Bravebot|Brave-Search))|(?:Yandex)|(?:Twitterbot)|(?:Discordbot)|(?:(Slackbot|Slack-ImgProxy))|(?:TelegramBot)|(?:(Pinterest|Pinterestbot))|(?:Snapchat)|(?:Redditbot)|(?:Tumblr)|(?:(Mastodon|http\.rb))|(?:Viber)|(?:^Line\/)|(?:WhatsApp)|(?:(SkypeUriPreview|Skype))|(?:(Teams|MicrosoftPreview))|(?:Zoom)|(?:Notion)|(?:(Quora|QuoraBot|Quora-Bot|Poe-Bot))|(?:Medium)|(?:(Pocket|PocketParser|PocketImageCache))|(?:(Flipboard|FlipboardProxy))|(?:(Embedly|embed\.ly))|(?:YelpBot)|(?:(Baiduspider|Baidu))|(?:(Sogou|sogou))|(?:(Yeti|NaverBot|Naver))|(?:SeznamBot)|(?:(Qwantify|Qwant))|(?:Ecosia)|(?:MojeekBot)|(?:Mail\.RU_Bot)|(?:(Yahoo! Slurp|Y!J-))|(?:(SemrushBot|SEMrush))|(?:(AhrefsBot|AhrefsSiteAudit))|(?:(DotBot|Rogerbot|moz\.com))|(?:MJ12bot)|(?:Screaming Frog)|(?:SISTRIX)|(?:SEOkicks)|(?:serpstatbot)|(?:RSiteAuditor)|(?:(?:Encited|LvHTML)-SEOAuditBot)|(?:Barkrowler)|(?:HubSpot Crawler)|(?:(AwarioSmartBot|AwarioBot))|(?:DataForSeoBot)|(?:SERanking)|(?:Seobility)/i
  FORWARD_HEADERS = %w[
    accept-language sec-fetch-mode sec-fetch-site sec-fetch-dest sec-fetch-user
    upgrade-insecure-requests referer user-agent
  ].freeze
  RESPONSE_HEADERS = %w[
    x-lovablehtml-render-cache x-lovablehtml-snapshot-key cache-control etag
    content-digest signature-input signature
  ].freeze

  def initialize(app)
    @app = app
  end

  def call(env)
    request = Rack::Request.new(env)
    accept = request.get_header("HTTP_ACCEPT").to_s
    user_agent = request.user_agent.to_s
    is_html = accept.empty? || accept == "*/*" || accept.include?("text/html")
    return @app.call(env) unless request.get? && is_html && user_agent.match?(CRAWLER)

    public_url = request.url
    uri = URI("https://encited.com/api/prerender/render")
    uri.query = URI.encode_www_form(url: public_url)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    http.open_timeout = 2
    http.read_timeout = 30
    rendered = http.start do
      req = Net::HTTP::Get.new(uri)
      # Set the LOVABLEHTML_API_KEY environment variable before starting.
      req["x-lovablehtml-api-key"] = ENV.fetch("LOVABLEHTML_API_KEY")
      req["accept"] = "text/html"
      FORWARD_HEADERS.each do |name|
        rack_name = "HTTP_#{name.upcase.tr('-', '_')}"
        value = request.get_header(rack_name).to_s
        req[name] = value unless value.empty?
      end
      http.request(req)
    end

    if rendered.code.to_i == 301 && rendered["location"]
      return [301, { "location" => rendered["location"], "cache-control" => "no-store" }, []]
    end
    return @app.call(env) if rendered.code.to_i == 304
    if rendered.code.to_i == 200 && rendered["content-type"].to_s.include?("text/html")
      headers = { "content-type" => "text/html; charset=utf-8" }
      RESPONSE_HEADERS.each do |name|
        headers[name] = rendered[name] if rendered[name]
      end
      return [200, headers, [rendered.body]]
    end

    @app.call(env)
  rescue StandardError
    @app.call(env)
  end
end

# config/application.rb - before routes/auth middleware
# config.middleware.insert_before 0, EncitedPrerender

2. Insert it first#

Load the file and insert EncitedPrerender at position 0 so crawler requests are handled before cache or authentication middleware.

bin/rails middleware

3. Restart Rails#

Restart Puma, Passenger, or your deployment service.

Test the integration#

1. Send a crawler request

curl -sS -D - -o /dev/null \
  -A "Googlebot" \
  -H "Accept: text/html" \
  "https://your-domain.com/"

Look for a successful HTML response and the x-lovablehtml-render-cache header.

2. Confirm browser passthrough

curl -sS -D - -o /dev/null \
  -A "Mozilla/5.0" \
  -H "Accept: text/html" \
  -H "Accept-Language: en-US" \
  -H "Sec-Fetch-Mode: navigate" \
  -H "Sec-Fetch-Dest: document" \
  -H "Upgrade-Insecure-Requests: 1" \
  "https://your-domain.com/"

This request should be served by your existing website without an Encited render-cache header.

Common errors#

Rails cannot load EncitedPrerender

Ensure app/middleware is autoloaded or require the middleware file explicitly in the environment configuration before inserting it.

Authentication redirects crawlers before pre-rendering

Insert EncitedPrerender at index zero and confirm its position with bin/rails middleware.

Request and security behavior#

  • Only eligible GET requests for public HTML pages are considered for pre-rendering.
  • Along with the public page URL, only the API key and crawler-classification headers are sent to Encited.
  • Cookie and Authorization headers are never forwarded to Encited.
  • If Encited is unavailable or does not return rendered HTML, the request falls through to your website.

If your site sits behind a firewall, bot protection, or rate limiting, those rules block us before this middleware helps. Set a render token and add one firewall rule so renders and audit crawls get through.

Common questions#

How does pre-rendering work with Ruby on Rails middleware?#

A Rack middleware detects eligible crawler HTML requests, returns Encited rendered HTML, and passes browser traffic to the Rails application.

Where should the Rails pre-rendering middleware be registered?#

Insert EncitedPrerender at position zero so it runs before sessions, authentication, caching, and Rails route handling.

How do I test Googlebot pre-rendering in Rails?#

Request a public Rails URL with a Googlebot user agent and Accept: text/html, then inspect the response for x-lovablehtml-render-cache.