How Django pre-rendering works
Django can place Encited first in its middleware chain so crawler HTML requests are rendered before URL routing, views, and authentication.
Eligible crawler GET requests are handled before the Django view. Browser traffic and render failures call the next middleware and continue normally.
Prerequisites
- A Django deployment that can make outbound HTTPS requests.
- The requests package installed in the production environment.
- LOVABLEHTML_API_KEY configured for Gunicorn, uWSGI, Docker, or the hosting service.
Set up pre-rendering with Django
Install the HTTP client
The middleware uses requests for the outbound render call.
pip install requestsAdd the middleware first
Save the snippet in your project and put EncitedPrerenderMiddleware at the top of settings.py MIDDLEWARE.
Restart Django
Set LOVABLEHTML_API_KEY and restart Gunicorn, uWSGI, Docker, or your hosting deployment.
# myproject/middleware/encited_prerender.pyimport reimport osfrom urllib.parse import quoteimport requestsfrom django.http import HttpResponse, HttpResponsePermanentRedirectCRAWLER = re.compile(r"(?:(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))", re.IGNORECASE)FORWARD_HEADERS = ("accept-language", "sec-fetch-mode", "sec-fetch-site", "sec-fetch-dest","sec-fetch-user", "upgrade-insecure-requests", "referer", "user-agent",)RESPONSE_HEADERS = ("x-lovablehtml-render-cache", "x-lovablehtml-snapshot-key", "cache-control","etag", "content-digest", "signature-input", "signature",)class EncitedPrerenderMiddleware:def __init__(self, get_response):self.get_response = get_responsedef __call__(self, request):accept = request.headers.get("accept", "")user_agent = request.headers.get("user-agent", "")api_key = os.environ.get("LOVABLEHTML_API_KEY")is_html = not accept or accept == "*/*" or "text/html" in acceptif not api_key or request.method != "GET" or not is_html or not CRAWLER.search(user_agent):return self.get_response(request)endpoint = ("https://encited.com/api/prerender/render?url="+ quote(request.build_absolute_uri(), safe=""))try:headers = {"x-lovablehtml-api-key": api_key,"accept": "text/html",}for name in FORWARD_HEADERS:value = request.headers.get(name)if value:headers[name] = valuerendered = requests.get(endpoint,headers=headers,timeout=(2, 30),allow_redirects=False,)if rendered.status_code == 301 and rendered.headers.get("location"):return HttpResponsePermanentRedirect(rendered.headers["location"])if rendered.status_code == 304:return self.get_response(request)if rendered.status_code == 200 and "text/html" in rendered.headers.get("content-type", ""):response = HttpResponse(rendered.content,status=200,content_type="text/html; charset=utf-8",)for name in RESPONSE_HEADERS:value = rendered.headers.get(name)if value:response[name] = valuereturn responseexcept requests.RequestException:passreturn self.get_response(request)# settings.py - keep this first in MIDDLEWARE# MIDDLEWARE = ["myproject.middleware.encited_prerender.EncitedPrerenderMiddleware", ...]
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
Django raises ModuleNotFoundError for requests
Install requests in the production dependency set and rebuild the same environment that runs Django.
The middleware never sees crawler requests
Place EncitedPrerenderMiddleware at the top of settings.py MIDDLEWARE and restart every application worker.
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 Django middleware?
The first Django middleware checks crawler HTML requests, serves Encited rendered HTML, and delegates ordinary traffic to the remaining middleware chain.
Where should pre-rendering appear in Django MIDDLEWARE?
Place it first so sessions, authentication, redirects, and view middleware cannot handle eligible crawler requests before Encited.
Will Django admin or signed-in users reach Encited?
Normal browser requests continue to Django, and the integration does not forward Cookie or Authorization headers to Encited.
