How to Block AI Bots on RedwoodJS
RedwoodJS is a full-stack React framework built for startups and product teams — React frontend, GraphQL API, Prisma ORM, and first-class support for auth and testing. Unlike Next.js, Redwood has a strict web/api split: the web side is a React SPA (or RSC) built as static files, while the api side runs GraphQL services on Fastify. This split shapes how AI bot protection works — web-side blocking uses CDN headers and edge middleware, api-side blocking uses Fastify hooks or serverless function logic. This guide covers all four layers across both sides of a Redwood application.
web/ (React SPA — deployed to CDN or SSR) and api/ (GraphQL + Fastify — deployed to serverless functions or a Node server). Sections 1–4 cover the web side. Section 5 covers the api side.1. robots.txt
Static files in web/public/ are copied to the build output directory by Redwood's build process and served at the root URL by your deployment platform. Place robots.txt here for zero-config static serving.
Static robots.txt
Create web/public/robots.txt:
# Block AI training crawlers
User-agent: GPTBot
Disallow: /
User-agent: ClaudeBot
Disallow: /
User-agent: Claude-Web
Disallow: /
User-agent: anthropic-ai
Disallow: /
User-agent: CCBot
Disallow: /
User-agent: Google-Extended
Disallow: /
User-agent: PerplexityBot
Disallow: /
User-agent: Applebot-Extended
Disallow: /
User-agent: Amazonbot
Disallow: /
User-agent: meta-externalagent
Disallow: /
User-agent: Bytespider
Disallow: /
User-agent: Diffbot
Disallow: /
# Allow standard search crawlers
User-agent: Googlebot
Allow: /
User-agent: Bingbot
Allow: /
User-agent: *
Allow: /Redwood copies web/public/ to the output at build time. When deployed to Netlify or Vercel, the file is available at yoursite.com/robots.txt without any routing configuration.
Dynamic robots.txt via Redwood Function
For environment-aware robots.txt, create a Redwood serverless function at api/src/functions/robots.ts:
// api/src/functions/robots.ts
import type { APIGatewayEvent, Context } from 'aws-lambda'
const AI_BOTS_DISALLOW = `User-agent: GPTBot
Disallow: /
User-agent: ClaudeBot
Disallow: /
User-agent: Claude-Web
Disallow: /
User-agent: anthropic-ai
Disallow: /
User-agent: CCBot
Disallow: /
User-agent: Google-Extended
Disallow: /
User-agent: PerplexityBot
Disallow: /
User-agent: Applebot-Extended
Disallow: /
User-agent: Amazonbot
Disallow: /
User-agent: meta-externalagent
Disallow: /
User-agent: Bytespider
Disallow: /
User-agent: Googlebot
Allow: /
User-agent: Bingbot
Allow: /
User-agent: *
Allow: /
`
export const handler = async (_event: APIGatewayEvent, _context: Context) => {
const isProduction = process.env.NODE_ENV === 'production'
return {
statusCode: 200,
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
body: isProduction
? AI_BOTS_DISALLOW
: 'User-agent: *
Disallow: /
',
}
}/.redwood/functions/robots by default (or /api/robots depending on your proxy config). This is not the same as /robots.txt. For crawlers to read robots.txt at the correct path, use the static file approach or configure your Netlify/Vercel redirects to map /robots.txt to the function URL.Redirect robots.txt to function (Netlify)
Add to web/public/_redirects:
/robots.txt /.redwood/functions/robots 2002. noai meta via Metadata component
RedwoodJS provides the Metadata component (from @redwoodjs/web) for managing document head tags. Add it to your layout for site-wide coverage.
In a layout
Redwood layouts live in web/src/layouts/. Add the Metadata component to your main layout:
// web/src/layouts/MainLayout/MainLayout.tsx
import { Metadata } from '@redwoodjs/web'
type MainLayoutProps = {
children?: React.ReactNode
}
const MainLayout = ({ children }: MainLayoutProps) => {
return (
<>
<Metadata
title="My Site"
description="Site description"
robots="noai, noimageai"
/>
<header>{/* nav */}</header>
<main>{children}</main>
<footer>{/* footer */}</footer>
</>
)
}
export default MainLayoutThe Metadata component renders a <meta name="robots" content="noai, noimageai" /> tag in the document <head>. It uses Redwood's head management system internally.
Per-page override
In individual page components, render Metadata to override the layout default:
// web/src/pages/BlogPostPage/BlogPostPage.tsx
import { Metadata } from '@redwoodjs/web'
const BlogPostPage = ({ id }: { id: number }) => {
return (
<>
{/* Allow AI indexing for public blog posts */}
<Metadata
title="Blog Post Title"
robots="index, follow"
/>
<article>{/* post content */}</article>
</>
)
}
export default BlogPostPageMetaTags component (alternative)
Older Redwood projects may use MetaTags instead of Metadata. The pattern is the same:
import { MetaTags } from '@redwoodjs/web'
// In layout or page:
<MetaTags
title="My Site"
description="Description"
robots="noai, noimageai"
/>3. X-Robots-Tag via platform headers
Since Redwood's web side is typically static files on a CDN, X-Robots-Tag headers are set at the hosting platform level — not in application code.
Netlify — _headers file
Create web/public/_headers. Netlify reads this file and applies headers to matching paths:
/*
X-Robots-Tag: noai, noimageai
X-Content-Type-Options: nosniffRedwood copies web/public/_headers to the build output automatically. After deploying, every response from your Netlify site includes the header — HTML, JS bundles, and API function responses.
Netlify — netlify.toml
Add to netlify.toml at the project root:
[build]
command = "yarn rw deploy netlify"
publish = "web/dist"
functions = "api/dist/functions"
[[headers]]
for = "/*"
[headers.values]
X-Robots-Tag = "noai, noimageai"Vercel — vercel.json
Add to vercel.json at the project root:
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "X-Robots-Tag",
"value": "noai, noimageai"
}
]
}
]
}Cloudflare Pages — _headers
Same as Netlify — create web/public/_headers with the same format. Cloudflare Pages natively reads _headers files from the publish directory.
4. Hard 403 via edge middleware
For the web side, hard 403 blocking requires an edge function or platform middleware — the React SPA itself can't intercept crawler requests.
Netlify Edge Function
Create netlify/edge-functions/bot-block.ts:
// netlify/edge-functions/bot-block.ts
import type { Context } from 'netlify:edge'
const AI_BOT_PATTERNS = [
'GPTBot', 'ClaudeBot', 'Claude-Web', 'anthropic-ai',
'CCBot', 'Google-Extended', 'PerplexityBot', 'Applebot-Extended',
'Amazonbot', 'meta-externalagent', 'Bytespider', 'Diffbot',
]
const EXEMPT_PATHS = ['/robots.txt', '/sitemap.xml', '/favicon.ico']
export default async function botBlock(
request: Request,
context: Context
) {
const url = new URL(request.url)
if (EXEMPT_PATHS.some((p) => url.pathname === p)) {
return context.next()
}
const ua = request.headers.get('user-agent') ?? ''
const isBot = AI_BOT_PATTERNS.some((p) =>
ua.toLowerCase().includes(p.toLowerCase())
)
if (isBot) {
return new Response('Forbidden', { status: 403 })
}
const response = await context.next()
response.headers.set('X-Robots-Tag', 'noai, noimageai')
return response
}
export const config = { path: '/*' }Register in netlify.toml:
[[edge_functions]]
path = "/*"
function = "bot-block"Vercel middleware
Create middleware.ts at the project root (not inside web/ or api/):
// middleware.ts (project root)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const AI_BOT_PATTERNS = [
'GPTBot', 'ClaudeBot', 'Claude-Web', 'anthropic-ai',
'CCBot', 'Google-Extended', 'PerplexityBot', 'Applebot-Extended',
'Amazonbot', 'meta-externalagent', 'Bytespider', 'Diffbot',
]
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
const exempt = ['/robots.txt', '/sitemap.xml', '/favicon.ico']
if (exempt.includes(pathname)) {
return NextResponse.next()
}
const ua = request.headers.get('user-agent') ?? ''
const isBot = AI_BOT_PATTERNS.some((p) =>
ua.toLowerCase().includes(p.toLowerCase())
)
if (isBot) {
return new NextResponse('Forbidden', { status: 403 })
}
const response = NextResponse.next()
response.headers.set('X-Robots-Tag', 'noai, noimageai')
return response
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}5. API side — Fastify hooks and functions
The api/ side runs on Fastify (for self-hosted Redwood) or as serverless functions (for Netlify/Vercel). Protect it separately from the web side.
Fastify preHandler hook (self-hosted)
Edit api/server.config.ts to add a Fastify hook that runs before every request:
// api/server.config.ts
import type { FastifyInstance } from 'fastify'
const AI_BOT_PATTERNS = [
'GPTBot', 'ClaudeBot', 'Claude-Web', 'anthropic-ai',
'CCBot', 'Google-Extended', 'PerplexityBot', 'Applebot-Extended',
'Amazonbot', 'meta-externalagent', 'Bytespider', 'Diffbot',
]
function isAIBot(ua: string): boolean {
const lower = ua.toLowerCase()
return AI_BOT_PATTERNS.some((p) => lower.includes(p.toLowerCase()))
}
export const config = {
fastifyConfig: async (fastify: FastifyInstance) => {
fastify.addHook('preHandler', async (request, reply) => {
const ua = request.headers['user-agent'] ?? ''
if (isAIBot(ua)) {
fastify.log.warn({ ua, path: request.url }, 'AI bot blocked')
return reply.code(403).send('Forbidden')
}
reply.header('X-Robots-Tag', 'noai, noimageai')
})
},
}Serverless function check (Netlify/Vercel)
Add a UA check at the top of critical serverless functions in api/src/functions/:
// api/src/functions/graphql.ts (or any function)
import type { APIGatewayEvent } from 'aws-lambda'
const AI_BOT_PATTERNS = [
'GPTBot', 'ClaudeBot', 'CCBot', 'Google-Extended',
'PerplexityBot', 'Bytespider', 'Diffbot',
]
function isAIBot(ua: string): boolean {
const lower = ua.toLowerCase()
return AI_BOT_PATTERNS.some((p) => lower.includes(p.toLowerCase()))
}
export const handler = async (event: APIGatewayEvent) => {
const ua = event.headers['user-agent'] ?? ''
if (isAIBot(ua)) {
return { statusCode: 403, body: 'Forbidden' }
}
// ... rest of function handler
}Redwood middleware (api side)
Redwood v8+ supports an api/src/middleware/ directory for middleware that runs before all functions. Create api/src/middleware/botBlock.ts:
// api/src/middleware/botBlock.ts
import type { MiddlewareRequest, MiddlewareResponse } from '@redwoodjs/vite/middleware'
const AI_BOT_PATTERNS = [
'GPTBot', 'ClaudeBot', 'Claude-Web', 'anthropic-ai',
'CCBot', 'Google-Extended', 'PerplexityBot', 'Applebot-Extended',
'Amazonbot', 'meta-externalagent', 'Bytespider', 'Diffbot',
]
export const botBlockMiddleware = (
req: MiddlewareRequest,
res: MiddlewareResponse,
next: () => void
) => {
const ua = req.headers.get('user-agent') ?? ''
const isBot = AI_BOT_PATTERNS.some((p) =>
ua.toLowerCase().includes(p.toLowerCase())
)
if (isBot) {
res.status = 403
res.body = 'Forbidden'
return
}
next()
}6. Deployment comparison
Redwood is designed for serverless deployment. The web side is static; the api side runs as functions. AI bot protection requirements differ by layer and platform.
| Platform | robots.txt | Meta tags | X-Robots-Tag | Hard 403 |
|---|---|---|---|---|
| Netlify | ✓ | ✓ | ✓ (_headers) | ✓ (Edge Function) |
| Vercel | ✓ | ✓ | ✓ (vercel.json) | ✓ (middleware.ts) |
| Cloudflare Pages | ✓ | ✓ | ✓ (_headers) | ✓ (Pages Function) |
| Render | ✓ | ✓ | ✓ (custom headers) | ✓ (Fastify hook) |
| Self-hosted (Fastify) | ✓ | ✓ | ✓ (Fastify hook) | ✓ (Fastify hook) |
Recommended setup (Netlify)
The most common Redwood deployment is Netlify. Combine these layers for comprehensive protection:
# Layers for Netlify Redwood deployment
1. web/public/robots.txt — Disallow rules (CDN-served, zero latency)
2. netlify/edge-functions/ — Hard 403 + X-Robots-Tag on all requests
3. web/src/layouts/MainLayout.tsx — noai meta via <Metadata robots="noai, noimageai" />
4. api/src/functions/graphql.ts — UA check before GraphQL processingFAQ
Where do I put robots.txt in a Redwood project?
Place robots.txt in web/public/. Redwood's build process copies everything in web/public/ to the build output directory. When deployed to Netlify or Vercel, it's served at yoursite.com/robots.txt with no configuration needed. For dynamic content, create a function in api/src/functions/robots.ts and add a redirect in web/public/_redirects: /robots.txt /.redwood/functions/robots 200.
How do I add noai meta tags in RedwoodJS?
Use the Metadata component from @redwoodjs/web in your layout: <Metadata robots="noai, noimageai" />. This injects a robots meta tag into the document head for every page using the layout. For per-page overrides, render Metadata in the individual page component with a different value — the page-level tag takes precedence.
How do I set X-Robots-Tag headers in RedwoodJS on Netlify?
Create web/public/_headers with:
/*
X-Robots-Tag: noai, noimageaiNetlify reads this file and applies the header to all matching responses. Redwood copies it to the build output automatically — no manual upload needed.
How is RedwoodJS different from Next.js for AI bot blocking?
Redwood has a strict web/api split — the web side is static files on a CDN, the api side is GraphQL on Fastify or serverless functions. Bot blocking on the web side requires platform features (edge functions, _headers files). Next.js has a unified runtime where middleware runs for all requests in one place. The Redwood approach requires configuring protection at multiple layers — web platform + api server — rather than a single middleware file.
Can I block AI bots in a RedwoodJS serverless function?
Yes. Check the user-agent header at the top of your function handler and return a 403 before processing: if (isAIBot(event.headers['user-agent'] ?? '')) return { statusCode: 403, body: "Forbidden" }. For broader coverage, use Redwood's middleware in api/src/middleware/ (v8+) to protect all functions from one place.
How do I block AI bots in self-hosted RedwoodJS using Fastify?
Add a preHandler hook in api/server.config.ts: fastify.addHook("preHandler", async (request, reply) => { ... }). Check request.headers['user-agent'] and call reply.code(403).send("Forbidden") for matched bots. The preHandler hook runs before every route handler on the API side.
Is your site protected from AI bots?
Run a free scan to check your robots.txt, meta tags, and overall AI readiness score.