CrawlPodScan your site

Flask integration

AIVisibility from ai_visibility.middleware.flask is a standard Flask extension — construct it with app directly, or defer with init_app() — that hooks an after_request handler to detect AI crawlers and optimize the HTML response served to them.

Install

pip install ai-visibility[flask]

Adds Flask (≥2.0) as a dependency. The core package remains dependency-free otherwise.

Extension setup

app.py
from flask import Flask
from ai_visibility.middleware.flask import AIVisibility

app = Flask(__name__)
ai_vis = AIVisibility(app, optimize=True, inject_schemas=True)

Or the deferred application-factory pattern, if your project constructs the app lazily:

extensions.py + app factory
# extensions.py
from ai_visibility.middleware.flask import AIVisibility
ai_vis = AIVisibility()

# app.py
from flask import Flask
from extensions import ai_vis

def create_app():
    app = Flask(__name__)
    ai_vis.init_app(app)
    return app

Every request runs through AIVisibility's after_request hook, reading request.headers.get("User-Agent") to detect a crawler and response.content_type to skip anything that isn't text/html — non-HTML responses (a JSON endpoint, a static file) have zero added overhead.

Configuration options

AIVisibility(
    app: Flask | None = None, *,
    optimize: bool = True,
    inject_schemas: bool = False,
    schemas: list[dict[str, Any]] | None = None,
    log_visits: bool = True,
    on_detect: Callable[[CrawlerInfo, str], None] | None = None,
    optimize_options: OptimizeOptions | None = None,
    analytics: CrawlerAnalytics | None = None,
)
ParameterTypeDefaultDescription
appFlask | NoneNoneAttach immediately, or pass None and call init_app(app) later.
optimizeboolTrueServe an optimized HTML snapshot to detected crawlers.
inject_schemasboolFalseInject schemas as JSON-LD for detected crawlers.
schemaslist[dict] | NoneNoneJSON-LD schema dicts to inject when inject_schemas is set. Static, set once at construction — see per-route schemas below for page-specific data.
log_visitsboolTrueLog detected crawler visits.
on_detectCallable[[CrawlerInfo, str], None] | NoneNoneCalled with (crawler, path) on every detected visit.
optimize_optionsOptimizeOptions | NoneNoneFine-grained stripping control — see the optimizer reference.
analyticsCrawlerAnalytics | NoneNonePass a CrawlerAnalytics built with your own AnalyticsStore to persist visits — unlike Django, Flask's constructor accepts this directly. Defaults to a fresh in-memory tracker.

Using schema builders in Flask routes

Build page-specific schema in the route handler and inject it directly with inject_schemas(), independent of the extension's own static schemas config:

routes.py
from flask import render_template, url_for
from ai_visibility import product_schema, inject_schemas

@app.route("/products/<slug>")
def product_detail(slug):
    product = get_product_or_404(slug)
    html = render_template("product_detail.html", product=product)

    schema = product_schema(
        name=product.name,
        description=product.description,
        price=product.price,
        price_currency="USD",
        availability="InStock" if product.in_stock else "OutOfStock",
        url=url_for("product_detail", slug=slug, _external=True),
    )
    return inject_schemas(html, [schema])

Serving llms.txt and robots.txt

routes.py
from flask import Response
from ai_visibility import generate_llms_txt, generate_robots_txt
from ai_visibility.types import LlmsTxtConfig, LlmsTxtSection, LlmsTxtLink, RobotsTxtConfig

@app.route("/llms.txt")
def llms_txt():
    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 Response(body, mimetype="text/plain")

@app.route("/robots.txt")
def robots_txt():
    body = generate_robots_txt(RobotsTxtConfig(
        block_training_bots=True,
        sitemap_urls=["https://acme.com/sitemap.xml"],
    ))
    return Response(body, mimetype="text/plain")

Blueprint usage

AIVisibility attaches its after_request hook to the whole app, not per-blueprint — so routes registered on any blueprint are already covered once the extension is initialized on the app. Generator routes above work identically inside a blueprint:

seo.py
from flask import Blueprint, Response
from ai_visibility import generate_robots_txt

seo_bp = Blueprint("seo", __name__)

@seo_bp.route("/robots.txt")
def robots_txt():
    return Response(generate_robots_txt(), mimetype="text/plain")

If a blueprint's own routes need to opt out of optimization entirely (a webhook receiver, an internal admin blueprint that happens to return HTML), there's no built-in per-route bypass in 0.5.0 — route those under a distinct path and gate on request.path inside on_detect/a custom after_request instead.

Flask-RESTful / Flask-RESTX integration

Same story as Django REST Framework: AIVisibility only touches text/html responses, so Flask-RESTful/Flask-RESTX API endpoints (which return JSON) are already unaffected — no exclusion config needed. It only matters for the HTML-serving parts of an app that also mounts a REST API alongside server-rendered pages.

Testing with AI crawler User-Agents

test_ai_visibility.py
GPTBOT_UA = "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.2"

def test_strips_script_tags_for_gptbot(client):
    response = client.get("/", headers={"User-Agent": GPTBOT_UA})
    assert b"<script>" not in response.data

def test_regular_visitor_untouched(client):
    response = client.get("/", headers={"User-Agent": "Mozilla/5.0 (normal browser)"})
    assert b"<script>" in response.data

client is Flask's standard app.test_client() fixture (or the pytest-flask client fixture, if used) — nothing ai-visibility-specific is needed beyond setting the header.

See the middleware API reference for the shared AIVisibilityCore both Django and FastAPI adapters also wrap, and the Django guide / the FastAPI guide for the other two frameworks.