Django integration
AIVisibilityMiddleware from ai_visibility.middleware.django detects AI crawlers, optionally serves them a stripped/JSON-LD-injected HTML response, and logs the visit — configured entirely through an AI_VISIBILITY dict in settings.py. Regular visitors pass through untouched.
Install
pip install ai-visibility[django]Adds Django (≥3.2) as a dependency. The core package remains dependency-free otherwise.
Middleware setup
Add AIVisibilityMiddleware to MIDDLEWARE — placement matters: it needs to run after Django has produced a response (it reads and can rewrite response.content), and early enough that no other middleware short-circuits or has already streamed the response. Near the top of the list, right after Django's own security/session middleware, is the safe default:
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"ai_visibility.middleware.django.AIVisibilityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
# ...the rest of your middleware
]Internally, the middleware reads request.META['HTTP_USER_AGENT'] to detect a crawler, and only touches the response body when Content-Type is text/html — non-HTML responses (JSON APIs, static files, redirects) pass through with zero overhead for every request, crawler or not.
AI_VISIBILITY settings
AI_VISIBILITY = {
"optimize": True, # strip scripts/styles/tracking for detected crawlers
"inject_schemas": True, # inject JSON-LD from "schemas" below
"schemas": [], # list[dict] — JSON-LD schema dicts, e.g. from ai_visibility.schema builders
"log_visits": True, # log each detected visit via CrawlerAnalytics
"analytics": True, # master switch; False also disables log_visits
"on_detect": None, # optional callable(crawler: CrawlerInfo, path: str) -> None
"optimize_options": None, # optional OptimizeOptions() for fine-grained stripping control
}| Key | Type | Default | Description |
|---|---|---|---|
optimize | bool | True | Serve an optimized (script/style/tracking-stripped) HTML snapshot to detected crawlers. |
inject_schemas | bool | False | Inject schemas as JSON-LD into responses served to detected crawlers. |
schemas | list[dict] | [] | JSON-LD schema dicts to inject when inject_schemas is set — built once at import time, not per-request (see below for per-view schemas instead). |
log_visits | bool | True | Log detected visits to an internal CrawlerAnalytics instance. |
analytics | bool | True | Master switch — False forces log_visits off regardless of its own value. |
on_detect | Callable[[CrawlerInfo, str], None] | None | None | Called with (crawler, path) on every detected visit — for custom side effects (your own logging table, a metric). |
optimize_options | OptimizeOptions | None | None | Fine-grained control over what optimize strips — see the optimizer reference. |
The schemas list here is static— it's read once when the middleware is constructed at process start, so it's suited to site-wide schema (an Organization or WebSite schema), not per-page data. For page-specific schema (an Article on a blog post, a Product on a product page), inject it in the view instead — next section.
Adding JSON-LD schemas to specific views
Build the schema in the view and inject it directly with inject_schemas()— this works whether or not the middleware's own inject_schemas setting is on, since it operates on the already-rendered HTML string:
from django.http import HttpResponse
from django.template.loader import render_to_string
from ai_visibility import article_schema, inject_schemas
def post_detail(request, slug):
post = get_object_or_404(Post, slug=slug)
html = render_to_string("blog/post_detail.html", {"post": post}, request=request)
schema = article_schema(
headline=post.title,
description=post.excerpt,
author_name=post.author.get_full_name(),
date_published=post.published_at.isoformat(),
url=request.build_absolute_uri(),
)
return HttpResponse(inject_schemas(html, [schema]))This runs for every visitor, not just detected crawlers — JSON-LD is inert markup with no rendering cost, so there's no reason to gate it behind bot detection the way HTML optimization is gated.
Serving llms.txt and robots.txt
Plain function-based views returning text/plain, wired up like any other URL pattern:
from django.http import HttpResponse
from ai_visibility import generate_llms_txt, generate_robots_txt
from ai_visibility.types import LlmsTxtConfig, LlmsTxtSection, LlmsTxtLink, RobotsTxtConfig
def llms_txt(request):
body = generate_llms_txt(LlmsTxtConfig(
title="Acme",
summary="Acme makes widgets.",
sections=[LlmsTxtSection(name="Docs", links=[
LlmsTxtLink(title="Getting started", url="https://acme.com/docs"),
])],
))
return HttpResponse(body, content_type="text/plain; charset=utf-8")
def robots_txt(request):
body = generate_robots_txt(RobotsTxtConfig(
block_training_bots=True,
sitemap_urls=["https://acme.com/sitemap.xml"],
))
return HttpResponse(body, content_type="text/plain; charset=utf-8")from django.urls import path
from . import views
urlpatterns = [
path("llms.txt", views.llms_txt),
path("robots.txt", views.robots_txt),
# ...
]There is no dedicated Django template tag library for schema/generator output in 0.5.0 — every example above is plain Python called from a view, which composes with any Django project regardless of templating setup.
Django REST Framework integration notes
AIVisibilityMiddleware only acts on text/html responses, so it's already a no-op for a DRF API's JSON responses — nothing to configure or exclude. Where GEO actually applies to a DRF-backed project is the HTML-rendering side: server-rendered detail pages (or a DRF-powered SPA's prerendered/SSR shell) are what benefit from optimize_html()/schema injection, not the API endpoints themselves — those aren't what an AI crawler fetches or cites.
Testing with AI crawler User-Agents
Django's test client accepts arbitrary headers on any request — pass HTTP_USER_AGENT to simulate a known crawler and assert on the optimized response:
from django.test import TestCase
GPTBOT_UA = "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.2"
class AIVisibilityMiddlewareTests(TestCase):
def test_strips_script_tags_for_gptbot(self):
response = self.client.get("/", HTTP_USER_AGENT=GPTBOT_UA)
self.assertNotIn(b"<script>", response.content)
def test_regular_visitor_untouched(self):
response = self.client.get("/", HTTP_USER_AGENT="Mozilla/5.0 (normal browser)")
self.assertIn(b"<script>", response.content)For a real GPTBot User-Agent string to test against, use one from the crawler registry rather than inventing one — detect_crawler() matches on the exact lowercase substrings published there.
See the middleware API reference for the shared AIVisibilityCore/MiddlewareConfig both Flask and FastAPI adapters also wrap, and the Flask guide / the FastAPI guide for the other two frameworks.