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
Add the Rack middleware
Save the snippet as app/middleware/encited_prerender.rb. It only uses Ruby's standard HTTP libraries.
Insert it first
Load the file and insert EncitedPrerender at position 0 so crawler requests are handled before cache or authentication middleware.
bin/rails middlewareRestart Rails
Set LOVABLEHTML_API_KEY through your process manager and restart Puma, Passenger, or your deployment service.
# app/middleware/encited_prerender.rbrequire "net/http"require "uri"class EncitedPrerenderCRAWLER = /(?:(OAI-SearchBot|ChatGPT-User(?:\/\d+\.\d+)?|GPTBot|ChatGPT|OpenAI-))|(?:(anthropic-ai|ClaudeBot|claude-web|Claude-Web|Claude-User|Claude-SearchBot))|(?:(GrokBot|xAI-Grok))|(?:(PerplexityBot|Perplexity-User))|(?:(Google-InspectionTool|GoogleOther(?:-Image|-Video)?|Googlebot|Google-CloudVertexBot|Storebot-Google))|(?:(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))|(?:Bytespider)|(?:(DuckAssistBot|DuckDuckBot))|(?:(cohere-training-data-crawler|cohere-ai|CohereAI))|(?:MistralAI-User)|(?:AI2Bot)|(?:CCBot)|(?:Diffbot)|(?:omgili)|(?:TimpiBot)|(?:YouBot)|(?:(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))|(?:(Baiduspider|Baidu))|(?:(Sogou|sogou))|(?:(Yeti|NaverBot|Naver))|(?:SeznamBot)|(?:(Qwantify|Qwant))|(?:Ecosia)|(?:MojeekBot)|(?:Mail\.RU_Bot)|(?:Yahoo! Slurp)|(?:(SemrushBot|SEMrush))|(?:(AhrefsBot|AhrefsSiteAudit))|(?:(DotBot|Rogerbot|moz\.com))|(?:MJ12bot)|(?:Screaming Frog)|(?:SISTRIX)|(?:SEOkicks)|(?:serpstatbot)|(?:RSiteAuditor)|(?:LvHTML-SEOAuditBot)|(?:Barkrowler)|(?:HubSpot Crawler)|(?:(AwarioSmartBot|AwarioBot))/iFORWARD_HEADERS = %w[accept-language sec-fetch-mode sec-fetch-site sec-fetch-dest sec-fetch-userupgrade-insecure-requests referer user-agent].freezeRESPONSE_HEADERS = %w[x-lovablehtml-render-cache x-lovablehtml-snapshot-key cache-control etagcontent-digest signature-input signature].freezedef initialize(app)@app = appenddef call(env)request = Rack::Request.new(env)accept = request.get_header("HTTP_ACCEPT").to_suser_agent = request.user_agent.to_sis_html = accept.empty? || accept == "*/*" || accept.include?("text/html")return @app.call(env) unless request.get? && is_html && user_agent.match?(CRAWLER)public_url = request.urluri = 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 = truehttp.open_timeout = 2http.read_timeout = 30rendered = http.start doreq = Net::HTTP::Get.new(uri)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_sreq[name] = value unless value.empty?endhttp.request(req)endif rendered.code.to_i == 301 && rendered["location"]return [301, { "location" => rendered["location"], "cache-control" => "no-store" }, []]endreturn @app.call(env) if rendered.code.to_i == 304if 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]endreturn [200, headers, [rendered.body]]end@app.call(env)rescue StandardError@app.call(env)endend# config/application.rb - before routes/auth middleware# config.middleware.insert_before 0, EncitedPrerender
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.
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.
