Encited

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.