How to Block AI Bots on Nitro: Complete 2026 Guide
Nitro is the universal server engine powering Nuxt 3, Analog, TanStack Start, and standalone deployments. Blocking AI bots uses two primitives: defineNitroPlugin for a startup-registered request hook, and server/middleware/ for auto-loaded per-request handlers — both fire before any route handler.
Plugin vs middleware — pick one
Plugin (plugins/block-bots.ts): registers a 'request' hook at startup. More explicit lifecycle intent. Middleware (server/middleware/block-bots.ts): auto-loaded defineEventHandler, runs on every request in alphabetical order. Simpler syntax. Both block identically before any route — don't use both for the same concern.
Protection layers
Step 1 — Shared util (server/utils/ai-bots.ts)
Files in server/utils/ are auto-imported by Nitro — no import statement needed in your plugins or handlers (though explicit imports work too).
// server/utils/ai-bots.ts
export const AI_BOTS = [
// OpenAI
'gptbot', 'chatgpt-user', 'oai-searchbot',
// Anthropic
'claudebot', 'claude-web',
// Common Crawl
'ccbot',
// Bytedance
'bytespider',
// Meta
'meta-externalagent',
// Perplexity
'perplexitybot',
// Google AI
'google-extended', 'googleother',
// Cohere
'cohere-ai',
// Amazon
'amazonbot',
// Diffbot
'diffbot',
// AI2
'ai2bot',
// DeepSeek
'deepseekbot',
// Mistral
'mistralai-user',
// xAI
'xai-bot',
// You.com
'youbot',
// DuckDuckGo AI
'duckassistbot',
] as const;
export function isAiBot(userAgent: string | null | undefined): boolean {
if (!userAgent) return false;
const ua = userAgent.toLowerCase();
return AI_BOTS.some((bot) => ua.includes(bot));
}Step 2A — Plugin-based global blocker
Nitro auto-loads all files in plugins/ at startup. The 'request' hook fires on every incoming request before routing.
// plugins/block-bots.ts
// Runs once at startup — registers a request hook that fires on every request
import { isAiBot } from '~/server/utils/ai-bots';
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('request', async (event) => {
const ua = getRequestHeader(event, 'user-agent');
// 1. Block AI bots with a hard 403
if (isAiBot(ua)) {
throw createError({
statusCode: 403,
statusMessage: 'Forbidden',
});
}
// 2. Set X-Robots-Tag on all non-blocked responses
// (fires before route handler returns, so it applies to all responses)
setResponseHeader(event, 'X-Robots-Tag', 'noai, noimageai');
});
});Step 2B — Middleware-based global blocker (alternative)
Create server/middleware/block-bots.ts. Nitro auto-runs all files in server/middleware/on every request, in alphabetical order, before route handlers. Use this or the plugin — not both.
// server/middleware/block-bots.ts
// Nitro auto-runs all files in server/middleware/ on every request, in alphabetical order.
// This file runs before any route handler.
import { isAiBot } from '~/server/utils/ai-bots';
export default defineEventHandler((event) => {
const ua = getRequestHeader(event, 'user-agent');
if (isAiBot(ua)) {
throw createError({
statusCode: 403,
statusMessage: 'Forbidden',
});
}
// Add X-Robots-Tag to every response that passes the UA check
setResponseHeader(event, 'X-Robots-Tag', 'noai, noimageai');
});Step 3 — robots.txt route
Nitro maps server/routes/robots.txt.ts to GET /robots.txt automatically. No router configuration needed. Alternatively, place a static robots.txt in public/ — static files are served before server routes.
// server/routes/robots.txt.ts
// Nitro maps server/routes/robots.txt.ts → GET /robots.txt automatically.
// No router config needed — the filename IS the route.
const ROBOTS_CONTENT = `User-agent: *
Allow: /
# AI training bots — blocked
User-agent: GPTBot
Disallow: /
User-agent: ClaudeBot
Disallow: /
User-agent: CCBot
Disallow: /
User-agent: Bytespider
Disallow: /
User-agent: Google-Extended
Disallow: /
User-agent: PerplexityBot
Disallow: /
User-agent: Meta-ExternalAgent
Disallow: /
User-agent: YouBot
Disallow: /
User-agent: AmazonBot
Disallow: /
User-agent: Diffbot
Disallow: /`;
export default defineEventHandler((event) => {
setResponseHeader(event, 'Content-Type', 'text/plain; charset=utf-8');
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400');
return ROBOTS_CONTENT;
});Step 4 — Per-route blocking (optional)
When you only want to block AI bots on specific API routes, skip the global middleware and check inline. Useful for protecting a /api/private endpoint while leaving /api/public open.
// server/api/data.get.ts — per-route bot blocking
// Use when you want to block AI bots on specific routes only,
// not globally. The global middleware is simpler for most use cases.
import { isAiBot } from '~/server/utils/ai-bots';
export default defineEventHandler((event) => {
if (isAiBot(getRequestHeader(event, 'user-agent'))) {
throw createError({ statusCode: 403, statusMessage: 'Forbidden' });
}
// Your route logic here
return { data: 'protected content' };
});Step 5 — Dynamic block-list via useStorage()
Nitro's built-in useStorage() works with filesystem, Redis, Cloudflare KV, and other drivers. Store your block-list there and update it without a server restart. A TTL cache keeps DB reads off the hot path.
// plugins/block-bots-dynamic.ts — block-list from a DB or KV
// Module-level cache — persists for the lifetime of the server process
let cachedBotList: string[] | null = null;
let cacheExpiresAt = 0;
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
async function getBotList(storage: Storage): Promise<string[]> {
const now = Date.now();
if (cachedBotList && now < cacheExpiresAt) return cachedBotList;
// Read from Nitro storage (file-system, Redis, KV, etc.)
const raw = await storage.getItem<string[]>('blocked:bots');
cachedBotList = raw ?? ['gptbot', 'claudebot', 'ccbot', 'bytespider'];
cacheExpiresAt = now + CACHE_TTL_MS;
return cachedBotList;
}
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('request', async (event) => {
const ua = getRequestHeader(event, 'user-agent')?.toLowerCase() ?? '';
const botList = await getBotList(useStorage());
if (botList.some((pattern) => ua.includes(pattern))) {
throw createError({ statusCode: 403, statusMessage: 'Forbidden' });
}
setResponseHeader(event, 'X-Robots-Tag', 'noai, noimageai');
});
});
// Update block-list without restart:
// await useStorage().setItem('blocked:bots', ['gptbot', 'claudebot', 'newbot'])Nitro vs Hono vs Bun vs Cloudflare Workers
| Feature | Nitro | Hono | Bun HTTP | CF Workers |
|---|---|---|---|---|
| Runtime | Universal (Node, Bun, Deno, CF, Vercel…) | Universal (same presets via adapter) | Bun.serve() — Bun runtime only | V8 isolate — Cloudflare only |
| Global middleware | defineNitroPlugin hook or server/middleware/ | app.use("*", middleware) | Manual check in Bun.serve() fetch() | export default { fetch() {} } |
| UA header access | getRequestHeader(event, 'user-agent') | c.req.header('user-agent') | req.headers.get('user-agent') | request.headers.get('user-agent') |
| Hard 403 | createError({ statusCode: 403 }) | c.text('Forbidden', 403) | new Response('Forbidden', { status: 403 }) | new Response('Forbidden', { status: 403 }) |
| robots.txt | server/routes/robots.txt.ts (auto-mapped) | app.get('/robots.txt', handler) | Manual route in Bun.serve() | Workers Assets or explicit route |
| X-Robots-Tag | setResponseHeader(event, 'X-Robots-Tag', …) | c.header('X-Robots-Tag', …) | headers.set('X-Robots-Tag', …) | headers.set('X-Robots-Tag', …) |
| Dynamic block-list | useStorage() — fs, Redis, KV, or custom driver | External DB or KV via adapter env | External DB via Bun SQL or fetch | Workers KV or D1 |
| CF Workers deploy | nitro preset: "cloudflare-module" | Native — Hono was built for CF Workers | Not supported | Native |
Quick reference
getRequestHeader(event, 'user-agent')createError({ statusCode: 403, statusMessage: 'Forbidden' })setResponseHeader(event, 'X-Robots-Tag', 'noai, noimageai')plugins/block-bots.ts (auto-loaded)server/middleware/block-bots.ts (auto-loaded, alpha order)server/routes/robots.txt.ts → /robots.txtpublic/robots.txt (served before server routes)useStorage().getItem('blocked:bots')FAQ
How do I block AI bots globally on Nitro?
Two approaches — pick one. Plugin: create plugins/block-bots.ts with defineNitroPlugin(nitroApp => { nitroApp.hooks.hook('request', async event => { ... })}). Middleware: create server/middleware/block-bots.ts with defineEventHandler. Both fire before any route handler. In both cases, check getRequestHeader(event, 'user-agent') and throw createError({ statusCode: 403 }) for AI bots.
What is the difference between a Nitro plugin and server middleware?
Plugins (plugins/) run once at server startup and register lifecycle hooks via nitroApp.hooks. Middleware (server/middleware/) are defineEventHandler functions that Nitro auto-runs on every request in alphabetical filename order, before route handlers. Plugins are more explicit about lifecycle intent; middleware has simpler syntax. For AI bot blocking, both work identically — don't use both for the same concern.
How do I serve robots.txt in Nitro?
Two options: (1) Static file — place robots.txt in public/. Nitro serves static files before server routes — fastest, no handler needed. (2) Route handler — create server/routes/robots.txt.ts. Nitro maps the filename to the URL path automatically. Use the static file for simple cases; the route handler when you need dynamic content (per-environment, per-deployment).
How is Nitro different from Nuxt for blocking AI bots?
Nuxt 3 is built on top of Nitro. The Nitro patterns — defineEventHandler, defineNitroPlugin, getRequestHeader, createError — work identically in both. In Nuxt you additionally have @nuxtjs/robots and useHead(). In standalone Nitro, you use only the Nitro primitives, which are portable across all Nitro presets (Node, Bun, Deno, Cloudflare, Vercel Edge, AWS Lambda).
Does Nitro middleware work on Cloudflare Workers and Vercel Edge?
Yes. defineEventHandler, getRequestHeader, and createError are universal — the same code compiles via Nitro's preset system to Cloudflare Workers (preset: "cloudflare-module"), Vercel Edge (preset: "vercel-edge"), AWS Lambda, Bun, Deno, and Node.js. Write the middleware once — it runs everywhere Nitro deploys. Exception: dynamic block-lists using useStorage() require a preset-compatible driver (e.g. Cloudflare KV driver on Cloudflare Workers).
Is your site protected from AI bots?
Run a free scan to check your robots.txt, meta tags, and overall AI readiness score.