How to Integrate Pre-rendering with Django
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#
1. Install the HTTP client#
The middleware uses requests for the outbound render call.
pip install requests
2. Add the middleware first#
Save the snippet in your project and put EncitedPrerenderMiddleware at the top of settings.py MIDDLEWARE.
encited_prerender
# myproject/middleware/encited_prerender.py
import re
import os
from urllib.parse import quote
import requests
from django.http import HttpResponse, HttpResponsePermanentRedirect
CRAWLER = re.compile(r"(?:(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)", 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_response
def __call__(self, request):
accept = request.headers.get("accept", "")
user_agent = request.headers.get("user-agent", "")
# Set the LOVABLEHTML_API_KEY environment variable before starting.
api_key = os.environ.get("LOVABLEHTML_API_KEY")
is_html = not accept or accept == "*/*" or "text/html" in accept
if 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] = value
rendered = 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] = value
return response
except requests.RequestException:
pass
return self.get_response(request)
# settings.py - keep this first in MIDDLEWARE
# MIDDLEWARE = ["myproject.middleware.encited_prerender.EncitedPrerenderMiddleware", ...]
3. Restart Django#
Restart Gunicorn, uWSGI, Docker, or your hosting deployment.
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.
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 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.
