How to Block AI Bots on Vike
Vike (formerly vite-plugin-ssr) is a Vite-based SSR framework with a philosophy of minimal magic: it provides SSR primitives and lets you bring your own server, UI framework (React, Vue, Solid, Preact), and routing strategy. Unlike Next.js or SvelteKit, Vike doesn't have a built-in middleware system — you write a standard Express, Fastify, or Hono server and call renderPage() for matched routes. This means AI bot blocking is standard server middleware with no framework-specific APIs to learn. This guide covers robots.txt, noai meta via Vike's +Head file convention, X-Robots-Tag and hard 403 in your custom server, and all major deployment targets.
server/index.ts or similar). Your middleware runs there before renderPage(). This makes bot blocking identical to any Node.js server — no Vike-specific API needed.1. robots.txt
Vike uses Vite as its build tool. Files in public/ are served at the root URL in development (via Vite dev server) and copied to the build output for production.
Static robots.txt
Create 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: /Dynamic robots.txt via custom server route
In your server file, add a dedicated route before the Vike renderPage() catch-all. This is the same pattern for Express, Fastify, and Hono:
// server/index.ts (Express example)
import express from 'express'
import { renderPage } from 'vike/server'
const app = express()
// robots.txt — serve before Vike handles routes
app.get('/robots.txt', (req, res) => {
const isProduction = process.env.NODE_ENV === 'production'
res.type('text/plain')
res.send(
isProduction
? `User-agent: GPTBot
Disallow: /
User-agent: ClaudeBot
Disallow: /
User-agent: CCBot
Disallow: /
User-agent: Google-Extended
Disallow: /
User-agent: PerplexityBot
Disallow: /
User-agent: Googlebot
Allow: /
User-agent: Bingbot
Allow: /
User-agent: *
Allow: /`
: 'User-agent: *
Disallow: /
'
)
})
// Vike handles all other routes
app.get('*', async (req, res) => {
const pageContext = await renderPage({ urlOriginal: req.originalUrl })
// ... send response
})2. noai meta via +Head files
Vike uses a file-based convention where special + prefix files define page behaviour. The +Head file controls what goes into the HTML <head> for pages in that directory and all subdirectories.
Global default — pages/+Head.tsx (React)
// pages/+Head.tsx — applies to all pages
export default function HeadDefault() {
return (
<>
{/* AI bot protection — site-wide default */}
<meta name="robots" content="noai, noimageai" />
</>
)
}Per-page override — pages/blog/@slug/+Head.tsx
// pages/blog/@slug/+Head.tsx
import { usePageContext } from 'vike-react/usePageContext'
export default function Head() {
const { data } = usePageContext()
return (
<>
<title>{data.title}</title>
{/* Override: allow indexing for public blog posts */}
<meta name="robots" content="index, follow" />
</>
)
}+Head files from all matching directories (root → current page). All of them are rendered. When the same meta name appears in both the root +Head and a page-level +Head, both tags appear in the HTML — browsers use the last matching meta tag. Place your per-page override after the global one by ensuring page-level +Head files render their meta tags (they render after the root level).Data-driven robots from +data.ts
For CMS-driven robots values, load the data in +data.ts and use it in +Head:
// pages/articles/@slug/+data.ts
export async function data(pageContext) {
const article = await fetchArticle(pageContext.routeParams.slug)
return article
}
// pages/articles/@slug/+Head.tsx
import { useData } from 'vike-react/useData'
export default function Head() {
const { robots, title } = useData()
return (
<>
<title>{title}</title>
<meta name="robots" content={robots ?? 'noai, noimageai'} />
</>
)
}3. X-Robots-Tag in the custom server
Since Vike requires a custom server, adding response headers is plain server middleware — no framework-specific API needed.
Express
// server/index.ts
import express from 'express'
import { renderPage } from 'vike/server'
const app = express()
// Add X-Robots-Tag to all HTML responses
app.use((req, res, next) => {
res.on('finish', () => {
// Can't set headers after response is finished — use beforehand:
})
next()
})
// Better: set header before rendering
app.get('*', async (req, res) => {
const pageContext = await renderPage({ urlOriginal: req.originalUrl })
if (!pageContext.httpResponse) {
return next()
}
const { body, statusCode, headers } = pageContext.httpResponse
// Set headers from Vike, then add our own
headers.forEach(([name, value]) => res.setHeader(name, value))
res.setHeader('X-Robots-Tag', 'noai, noimageai')
res.status(statusCode).send(body)
})Fastify
// server/index.ts (Fastify)
import Fastify from 'fastify'
import { renderPage } from 'vike/server'
const app = Fastify()
// Add header via hook — runs before reply is sent
app.addHook('onSend', async (request, reply, payload) => {
const contentType = reply.getHeader('content-type') as string | undefined
if (contentType?.includes('text/html')) {
reply.header('X-Robots-Tag', 'noai, noimageai')
}
return payload
})
app.get('*', async (request, reply) => {
const pageContext = await renderPage({ urlOriginal: request.url })
if (!pageContext.httpResponse) return reply.callNotFound()
const { body, statusCode, headers } = pageContext.httpResponse
headers.forEach(([name, value]) => reply.header(name, value))
reply.status(statusCode).send(body)
})Hono
// server/index.ts (Hono)
import { Hono } from 'hono'
import { renderPage } from 'vike/server'
const app = new Hono()
// Add header middleware
app.use('*', async (c, next) => {
await next()
if (c.res.headers.get('content-type')?.includes('text/html')) {
c.res.headers.set('X-Robots-Tag', 'noai, noimageai')
}
})
app.get('*', async (c) => {
const pageContext = await renderPage({ urlOriginal: c.req.url })
if (!pageContext.httpResponse) return c.notFound()
const { body, statusCode, headers } = pageContext.httpResponse
return new Response(body, {
status: statusCode,
headers: [
...headers,
['X-Robots-Tag', 'noai, noimageai'],
],
})
})4. Hard 403 before renderPage()
The most effective blocking strategy: check the User-Agent and return 403 before calling renderPage(). Vike never executes — no SSR, no database queries, no content served.
Express — complete bot-blocking server
// server/index.ts
import express from 'express'
import { renderPage } from 'vike/server'
const app = express()
const AI_BOT_PATTERNS = [
'GPTBot', 'ClaudeBot', 'Claude-Web', 'anthropic-ai',
'CCBot', 'Google-Extended', 'PerplexityBot', 'Applebot-Extended',
'Amazonbot', 'meta-externalagent', 'Bytespider', 'Diffbot',
'YouBot', 'cohere-ai',
]
const EXEMPT_PATHS = ['/robots.txt', '/sitemap.xml', '/favicon.ico']
function isAIBot(ua: string): boolean {
const lower = ua.toLowerCase()
return AI_BOT_PATTERNS.some((p) => lower.includes(p.toLowerCase()))
}
// Static files from public/ (robots.txt, etc.)
app.use(express.static('public'))
// AI bot blocking — runs before renderPage()
app.use((req, res, next) => {
if (EXEMPT_PATHS.some((p) => req.path === p)) {
return next()
}
const ua = req.headers['user-agent'] ?? ''
if (isAIBot(ua)) {
console.warn(`[blocked] ${req.method} ${req.path} | UA: ${ua}`)
return res.status(403).send('Forbidden')
}
next()
})
// Vike SSR — only reached by legitimate requests
app.get('*', async (req, res, next) => {
const pageContext = await renderPage({ urlOriginal: req.originalUrl })
if (!pageContext.httpResponse) return next()
const { body, statusCode, headers } = pageContext.httpResponse
headers.forEach(([name, value]) => res.setHeader(name, value))
res.setHeader('X-Robots-Tag', 'noai, noimageai')
res.status(statusCode).send(body)
})
const port = process.env.PORT ?? 3000
app.listen(port, () => console.log(`Server running on port ${port}`))express.static for the public/ directory before the bot-blocking middleware. This ensures public/robots.txt is served to all crawlers without going through the bot check — AI crawlers can always read your Disallow rules.Fastify — complete example
// server/index.ts (Fastify)
import Fastify from 'fastify'
import staticPlugin from '@fastify/static'
import { renderPage } from 'vike/server'
import path from 'path'
const app = Fastify({ logger: true })
// Serve public/ first (robots.txt, etc.)
app.register(staticPlugin, {
root: path.join(process.cwd(), 'public'),
decorateReply: false,
})
// Bot blocking hook — runs on every request
app.addHook('preHandler', async (request, reply) => {
const path = request.url
const exempt = ['/robots.txt', '/sitemap.xml', '/favicon.ico']
if (exempt.includes(path)) return
const ua = request.headers['user-agent'] ?? ''
if (isAIBot(ua)) {
app.log.warn({ ua, path }, 'AI bot blocked')
return reply.code(403).send('Forbidden')
}
})
// Vike SSR catch-all
app.get('*', async (request, reply) => {
const pageContext = await renderPage({ urlOriginal: request.url })
if (!pageContext.httpResponse) return reply.callNotFound()
const { body, statusCode, headers } = pageContext.httpResponse
headers.forEach(([name, value]) => reply.header(name, value))
reply.header('X-Robots-Tag', 'noai, noimageai')
reply.status(statusCode).send(body)
})5. React, Vue, and Solid differences
Vike is framework-agnostic. Server middleware is identical regardless of UI framework. Only the +Head file syntax differs:
React — pages/+Head.tsx
export default function HeadDefault() {
return <meta name="robots" content="noai, noimageai" />
}Vue — pages/+Head.vue
<template>
<meta name="robots" content="noai, noimageai" />
</template>Solid — pages/+Head.tsx
export default function HeadDefault() {
return <meta name="robots" content="noai, noimageai" />
}The server middleware (bot blocking, X-Robots-Tag) is identical across all three — it runs at the HTTP layer before any framework rendering occurs.
6. Deployment
Vike supports multiple deployment targets via adapters or custom server configuration. All protection layers work across all targets because blocking happens in the HTTP server layer, not the framework layer.
Node.js server (self-hosted)
# Build
vike build # or: vite build
# Run production server
node dist/server/index.jsVercel (serverless)
Use the Vike Vercel adapter (vike-vercel) or deploy with a vercel.json that routes all requests to your server function. For Vercel-specific header injection, add:
// vercel.json
{
"headers": [
{
"source": "/(.*)",
"headers": [{ "key": "X-Robots-Tag", "value": "noai, noimageai" }]
}
]
}Cloudflare Workers
Vike supports Cloudflare Workers via the vike-cloudflare adapter. The Hono server example above works directly on Cloudflare Workers with no changes — Hono runs natively on the Workers runtime.
| Platform | robots.txt | Meta tags | X-Robots-Tag | Hard 403 |
|---|---|---|---|---|
| Node.js (Express/Fastify) | ✓ | ✓ | ✓ | ✓ |
| Hono (any runtime) | ✓ | ✓ | ✓ | ✓ |
| Vercel | ✓ | ✓ | ✓ | ✓ |
| Netlify | ✓ | ✓ | ✓ | ✓ |
| Cloudflare Workers | ✓ | ✓ | ✓ | ✓ |
FAQ
How do I serve robots.txt with Vike?
Place robots.txt in public/ — Vite serves it at the root URL automatically in both development and production. For dynamic content, add a dedicated route in your custom server before the renderPage() catch-all: app.get("/robots.txt", (req, res) => res.type("text/plain").send("...")). Register express.static("public") first so the static file is served without hitting any middleware.
How do I add noai meta tags in Vike?
Create pages/+Head.tsx (React/Solid) or pages/+Head.vue (Vue) and add <meta name="robots" content="noai, noimageai" />. This applies to all pages. For per-page overrides, create a +Head file in the page's directory — Vike renders both, and browsers use the last matching meta tag.
Where does AI bot blocking happen in Vike?
In your custom server, add middleware before the renderPage() catch-all handler. When you return a 403 before calling renderPage(), Vike never executes — no SSR rendering, no component tree, no database queries. This is the most efficient approach. The onBeforeRender hook runs inside the Vike pipeline and can be used for redirects and access control, but the server middleware approach is faster for pure blocking.
How is Vike different from Next.js for bot blocking?
Next.js has a built-in middleware system (middleware.ts at the project root). Vike requires a custom server where bot blocking is standard HTTP middleware — no framework-specific API. This is more verbose but maximally portable: the same Express/Fastify/Hono middleware pattern works across all deployment targets, and the code is framework-agnostic (not tied to Vike internals).
Can Vike use different UI frameworks and still block AI bots the same way?
Yes. Server middleware is identical regardless of UI framework (React, Vue, Solid, Preact). Bot blocking happens at the HTTP layer before renderPage() is called — before any framework rendering. Only the +Head file syntax differs between frameworks (.tsx for React/Solid, .vue for Vue).
Does blocking AI bots with Vike affect Googlebot?
No. The UA patterns target AI training crawlers (GPTBot, ClaudeBot, CCBot, Google-Extended, PerplexityBot) — separate from Googlebot and Bingbot. Always place express.static("public") (or equivalent) before the bot-blocking middleware so all crawlers can read /robots.txt, and include explicit Allow: / rules for Googlebot and Bingbot.
Is your site protected from AI bots?
Run a free scan to check your robots.txt, meta tags, and overall AI readiness score.