Skip to content
Strapi · Koa.js · Headless CMS·9 min read

How to Block AI Bots on Strapi: Complete 2026 Guide

Strapi is a headless CMS — it delivers content via API, not HTML pages. This changes the bot-blocking picture significantly: noai meta tags belong on your frontend, not in Strapi. What Strapi does control: robots.txt (via public/), hard-blocking middleware (Koa), and X-Robots-Tag headers. The critical requirement is the same as Payload: never block /admin or /api.

Strapi is headless — read this first

Strapi serves your admin panel at /admin and your content API at /api. It does not serve your public-facing website. That means:

Methods at a glance

MethodWhat it doesWhere it lives
public/robots.txtSignals crawlers re: admin + APIStrapi public/ dir
Custom Koa middlewareHard 403 on bot User-Agentssrc/middlewares/
X-Robots-Tag middlewarenoai header on API responsessrc/middlewares/
Strapi Roles & PermissionsAuth-gate content API routesAdmin panel
nginx map blockHard 403 at reverse proxy layerServer config
noai <meta> tagAI training opt-out per pageYour frontend app

1. robots.txt — public/ directory

Strapi uses koa-static to serve files from the public/ directory at the root URL. Place robots.txt there and it will be served at /robots.txt — no config needed.

The robots.txt for your Strapi instance should address the admin panel and API — not your public website content (that lives in your frontend's robots.txt):

# public/robots.txt — in your Strapi project root
# This file is for your Strapi backend domain (e.g. api.example.com or cms.example.com)
# Your frontend (example.com) has its own robots.txt — see your frontend framework guide

# Block AI training bots from the admin panel
User-agent: GPTBot
Disallow: /admin
Disallow: /api

User-agent: ChatGPT-User
Disallow: /admin
Disallow: /api

User-agent: ClaudeBot
Disallow: /admin
Disallow: /api

User-agent: Claude-Web
Disallow: /admin
Disallow: /api

User-agent: anthropic-ai
Disallow: /admin
Disallow: /api

User-agent: Google-Extended
Disallow: /admin
Disallow: /api

User-agent: Bytespider
Disallow: /admin
Disallow: /api

User-agent: CCBot
Disallow: /admin
Disallow: /api

User-agent: PerplexityBot
Disallow: /admin
Disallow: /api

# Block all bots from admin — no bot should index the CMS panel
User-agent: *
Disallow: /admin

Two domains, two robots.txt files

If Strapi runs on api.example.com and your frontend runs on example.com, each domain needs its own robots.txt. The Strapi robots.txt governs the API domain; your frontend's robots.txt (in Next.js, Nuxt, etc.) governs the user-facing content. They are independent — updating one does not affect the other.

2. Hard 403 blocking — custom Koa middleware

Strapi's middleware system wraps Koa. Create a middleware file in src/middlewares/ and register it in config/middlewares.ts. The middleware runs on every request — exempt admin, API, GraphQL, health-check, and static files before checking User-Agent.

src/middlewares/block-ai-bots.ts

// src/middlewares/block-ai-bots.ts
import { Strapi } from "@strapi/strapi";

// Compiled once at module load — not per-request
const BLOCKED_UAS =
  /GPTBot|ChatGPT-User|OAI-SearchBot|ClaudeBot|Claude-Web|anthropic-ai|Google-Extended|Bytespider|CCBot|PerplexityBot|Applebot-Extended|meta-externalagent|Diffbot|ImagesiftBot/i;

// Paths that must never be blocked
const EXEMPT_PREFIXES = ["/admin", "/api", "/graphql", "/_health", "/robots.txt"];

export default (_config: unknown, { strapi: _strapi }: { strapi: Strapi }) => {
  return async (ctx: any, next: () => Promise<void>) => {
    const path = ctx.path;

    // Always pass through exempt paths
    if (EXEMPT_PREFIXES.some((prefix) => path.startsWith(prefix))) {
      return await next();
    }

    const ua: string = ctx.get("user-agent") ?? "";

    if (BLOCKED_UAS.test(ua)) {
      ctx.status = 403;
      ctx.body = "Forbidden";
      return; // Do not call next() — request ends here
    }

    await next();
  };
};

Why exempt /api?

Your /api routes power your frontend. If you block AI bot User-Agents from /api, any legitimate tool (testing, Postman, CI) using a custom User-Agent could break. The right way to protect content API routes from scrapers is Strapi Roles & Permissions — require authentication for sensitive endpoints instead of User-Agent blocking.

config/middlewares.ts — register the middleware

// config/middlewares.ts
export default [
  "strapi::logger",
  "strapi::errors",
  "strapi::security",
  "strapi::cors",
  "strapi::poweredBy",
  "strapi::query",
  "strapi::body",
  "strapi::session",
  "strapi::favicon",
  "strapi::public",          // serves public/ directory — keep before bot blocker
  {
    name: "global::block-ai-bots",
    config: {},
  },
];

Strapi v4 vs v5

The middleware format is identical in v4 and v5. v4 uses config/middlewares.js with module.exports = [...] (CommonJS); v5 supports both CommonJS and ES modules. The src/middlewares/ path and factory function signature are identical.

3. X-Robots-Tag on API responses

X-Robots-Tag is a response header. On JSON API responses it acts as a signal to bots that respect headers — some do check it before indexing API content. Add it in a separate Strapi middleware or combine it with the bot-blocking middleware.

// src/middlewares/x-robots-tag.ts
export default () => {
  return async (ctx: any, next: () => Promise<void>) => {
    await next();

    // Set X-Robots-Tag on all non-admin responses
    if (!ctx.path.startsWith("/admin")) {
      ctx.set("X-Robots-Tag", "noai, noimageai");
    }
  };
};

// config/middlewares.ts — add after the bot blocker
export default [
  // ... other middlewares
  { name: "global::block-ai-bots", config: {} },
  { name: "global::x-robots-tag", config: {} },
];

X-Robots-Tag on JSON vs HTML

The X-Robots-Tag header is primarily meaningful on HTML responses (web pages). On JSON API responses it is technically valid but most AI crawlers focus on HTML content. The header on your Strapi API is a belt-and-braces measure — the more important place is your frontend's HTML responses.

4. Protecting content from direct API scraping

If your Strapi API is public (no authentication required), any bot can query your content directly — bypassing your frontend's robots.txt and meta tags entirely. The definitive fix is Strapi's built-in Roles & Permissions.

Option A — Require authentication on sensitive endpoints

In the Strapi admin panel: Settings → Roles → Public. Uncheck find and findOne for any collection you want to protect. Only authenticated requests will be able to query those endpoints.

Option B — Block known AI bot User-Agents at /api/* in middleware

Less robust (bots can fake User-Agents) but appropriate as a second layer if your API must stay public. Update the middleware to optionally block bots from API routes:

// src/middlewares/block-ai-bots.ts — with optional API blocking
const BLOCKED_UAS =
  /GPTBot|ChatGPT-User|OAI-SearchBot|ClaudeBot|Claude-Web|anthropic-ai|Google-Extended|Bytespider|CCBot|PerplexityBot|Applebot-Extended/i;

export default (config: { blockApi?: boolean }, _ctx: any) => {
  return async (ctx: any, next: () => Promise<void>) => {
    // Always pass admin panel through
    if (ctx.path.startsWith("/admin")) return await next();

    // Optionally exempt /api (set blockApi: true in config to also block API routes)
    if (!config.blockApi && ctx.path.startsWith("/api")) return await next();

    if (ctx.path === "/robots.txt") return await next();

    const ua: string = ctx.get("user-agent") ?? "";
    if (BLOCKED_UAS.test(ua)) {
      ctx.status = 403;
      ctx.body = "Forbidden";
      return;
    }

    await next();
  };
};

// config/middlewares.ts
export default [
  // ...
  {
    name: "global::block-ai-bots",
    config: { blockApi: true }, // set true to also block known AI bots from /api/*
  },
];

5. noai meta tags — your frontend

Strapi does not render HTML pages for end users — your frontend does. Add noai meta tags in your frontend framework. Here are the guides for the most common Strapi frontend pairings:

If Strapi and your frontend run on the same domain (e.g., Strapi serving custom pages via custom routes), add the meta tag via a Strapi custom controller that renders HTML, or use a custom page template. This is uncommon — most Strapi setups are API-only.

6. nginx reverse proxy

Putting nginx in front of Strapi adds a network-level blocking layer before Node.js even sees the request. More efficient for high-traffic APIs — nginx handles the 403 response without loading the Node process.

# /etc/nginx/sites-available/strapi
map $http_user_agent $blocked_bot {
  default           0;
  "~*GPTBot"        1;
  "~*ChatGPT-User"  1;
  "~*OAI-SearchBot" 1;
  "~*ClaudeBot"     1;
  "~*Claude-Web"    1;
  "~*anthropic-ai"  1;
  "~*Google-Extended" 1;
  "~*Bytespider"    1;
  "~*CCBot"         1;
  "~*PerplexityBot" 1;
}

server {
  listen 80;
  server_name api.example.com;

  # Never block robots.txt
  location = /robots.txt {
    proxy_pass http://localhost:1337;
  }

  # Never block the admin panel from legitimate users
  location /admin {
    proxy_pass http://localhost:1337;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
  }

  # Block AI bots from public routes and API
  location / {
    if ($blocked_bot) {
      return 403 "Forbidden";
    }

    proxy_pass http://localhost:1337;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}

7. Deployment

PlatformNotesCustom middleware runs?
RailwayAuto-detect Node.js, add Postgres/MySQL addon, PORT env var
RenderWeb Service, add DATABASE_URL + STRAPI secrets as env vars
Strapi CloudManaged hosting by Strapi — custom middleware supported
VPS + nginxFull control, nginx in front for extra bot-blocking layer
DockerMulti-stage build, node:22-alpine, persist uploads via volume
HerokuLegacy option — ephemeral filesystem breaks media uploads

Docker — multi-stage build

# Dockerfile
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build   # npx strapi build

FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/build ./build
COPY --from=builder /app/config ./config
COPY --from=builder /app/public ./public
COPY --from=builder /app/src ./src
RUN npm ci --omit=dev

EXPOSE 1337
CMD ["npm", "start"]   # npx strapi start

# Mount a Docker volume for /app/public/uploads to persist media uploads

Frequently asked questions

How do I serve robots.txt in Strapi?

Place robots.txt in the public/ directory at your Strapi project root. Strapi uses koa-static to serve files from public/ at the root URL — no configuration needed.

How do I add bot-blocking middleware to Strapi?

Create src/middlewares/block-ai-bots.ts with a factory function that returns a Koa middleware. Register it in config/middlewares.ts as { name: "global::block-ai-bots", config: {} }. Always exempt /admin, /api, and /robots.txt.

Where do noai meta tags go in a Strapi project?

In your frontend, not Strapi. Strapi is headless — it delivers JSON, not HTML pages. Add <meta name="robots" content="noai, noimageai"> in your Next.js, Nuxt, Gatsby, or Astro layout. See the links in section 5 above.

Should I block AI bots from the Strapi REST API?

If your API is public, bots can scrape your content directly regardless of your frontend. The robust fix is authentication — use Strapi Roles & Permissions to require a token for sensitive endpoints. As a second layer, set blockApi: true in your middleware config to also block known AI bot User-Agents from /api/*.

How do I add X-Robots-Tag headers in Strapi?

Create a separate Strapi middleware (or add to your existing one) that calls ctx.set("X-Robots-Tag", "noai, noimageai") after await next(). Skip the header on /admin routes. Register it in config/middlewares.ts.

What is the difference between blocking bots in Strapi v4 vs v5?

The Koa middleware API is identical. v4 uses config/middlewares.js with CommonJS module.exports; v5 supports both CommonJS and ES modules (export default). The src/middlewares/ directory and factory function signature are the same in both.

Is your site protected from AI bots?

Run a free scan to check your robots.txt, meta tags, and overall AI readiness score.

Related Guides