How Nginx pre-rendering works
Nginx can combine maps, an Encited upstream, and the official njs module to route crawler HTML requests without application-code changes.
Nginx sends eligible crawler GET requests to Encited and keeps browsers, assets, and non-HTML requests on the original upstream. API failures fall back to the origin.
Prerequisites
- An Nginx build with the official nginx-module-njs package installed and loaded.
- Access to edit both the http and server configuration contexts.
- The Encited API key and the existing website origin upstream.
Set up pre-rendering with Nginx
Install and load the official njs module
The njs module safely URL-encodes the complete public URL before Nginx sends it as an Encited query parameter. Install the nginx-module-njs package for your distribution and load it in nginx.conf.
Add the JavaScript and Nginx sections
Save the first section as /etc/nginx/encited.js, then merge the remaining maps, upstream, and locations into the http and server contexts. Replace the API-key placeholder and origin upstream.
Test and reload Nginx
Validate the complete configuration before applying it.
nginx -t && nginx -s reload# /etc/nginx/encited.js - requires the official nginx njs modulefunction encodeTarget(r) {const publicUrl = r.variables.scheme + '://' + r.variables.host + r.variables.request_uri;return encodeURIComponent(publicUrl);}export default { encodeTarget };# nginx.conf - add the remaining configuration below to the http contextjs_import encited from /etc/nginx/encited.js;js_set $encited_target encited.encodeTarget;map $http_user_agent $encited_crawler {default 0;~*((?:(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))) 1;}map "$request_method:$encited_crawler:$http_accept" $encited_route {default 0;~^GET:1:(?:$|.*(?:text/html|\*/\*)) 1;}upstream your_existing_origin {# CHANGE THIS to the upstream that currently serves your website.server 127.0.0.1:3000;}server {location / {error_page 418 = @encited_prerender;if ($encited_route) { return 418; }proxy_pass http://your_existing_origin;}location @encited_origin {proxy_pass http://your_existing_origin;}location @encited_prerender {# Fail open to the existing origin for passthrough and API errors.proxy_intercept_errors on;error_page 304 400 401 402 403 404 408 422 429 500 501 502 503 504 = @encited_origin;proxy_ssl_server_name on;proxy_set_header Host encited.com;proxy_set_header x-lovablehtml-api-key <your-api-key>;proxy_set_header Accept text/html;proxy_set_header Accept-Language $http_accept_language;proxy_set_header Sec-Fetch-Mode $http_sec_fetch_mode;proxy_set_header Sec-Fetch-Site $http_sec_fetch_site;proxy_set_header Sec-Fetch-Dest $http_sec_fetch_dest;proxy_set_header Sec-Fetch-User $http_sec_fetch_user;proxy_set_header Upgrade-Insecure-Requests $http_upgrade_insecure_requests;proxy_set_header Referer $http_referer;proxy_set_header User-Agent $http_user_agent;proxy_set_header Cookie "";proxy_set_header Authorization "";proxy_pass https://encited.com/api/prerender/render?url=$encited_target;}}
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
Nginx reports an unknown js_set directive
Install and load the official njs module before including the Encited JavaScript and server configuration.
The fallback loops back through the pre-rendering location
Point the origin upstream at the real application backend, not the same public hostname handled by this Nginx server.
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 Nginx pre-rendering work with njs?
njs safely encodes the full public URL while Nginx routes eligible crawler HTML requests to Encited and everything else to the origin.
Does the Nginx integration send visitor cookies to Encited?
No. The configuration clears Cookie and Authorization headers before making the render request.
How do I validate an Nginx pre-rendering setup?
Run nginx -t before reloading, then send a Googlebot HTML request and inspect the response for the Encited render-cache header.
