How to Block AI Bots on SolidStart: Complete 2026 Guide
SolidStart is SolidJS's meta-framework — file-based routing, SSR by default, built on Vinxi. Bot blocking lives in src/middleware.ts via createMiddleware. This guide covers public/robots.txt static serving, hard-blocking middleware, noai meta tags via @solidjs/meta, setting X-Robots-Tag headers, SSR vs SPA mode differences, and deployment on Vercel, Netlify, and Node.
SolidStart 1.x (Vinxi)
All examples target SolidStart 1.x (the stable release built on Vinxi). SolidStart 0.x (Vite plugin era) used a different middleware API — if you are on 0.x, upgrade. The createMiddleware API is SolidStart 1.x only.
Methods at a glance
| Method | What it does | Blocks JS-less bots? |
|---|---|---|
| public/robots.txt | Signals crawlers to stay out | Signal only |
| @solidjs/meta <Meta> | noai training opt-out (SSR/SSG) | ✓ in SSR mode |
| Per-route <Meta> override | noai on specific pages only | ✓ in SSR mode |
| X-Robots-Tag in middleware | noai header on all responses | ✓ (header) |
| createMiddleware hard block | Hard 403 globally — before routing | ✓ |
| API route /robots.txt.ts | Dynamic robots.txt with env rules | Signal only |
| Platform edge rules | Vercel/Netlify/CF hard block | ✓ |
1. robots.txt — public/ directory
Drop robots.txt in public/. Vinxi copies everything in public/ verbatim to the build output — no configuration needed. The file is served at /robots.txt before any middleware or route handlers run.
public/robots.txt
User-agent: GPTBot
Disallow: /
User-agent: ChatGPT-User
Disallow: /
User-agent: OAI-SearchBot
Disallow: /
User-agent: ClaudeBot
Disallow: /
User-agent: Claude-Web
Disallow: /
User-agent: anthropic-ai
Disallow: /
User-agent: Google-Extended
Disallow: /
User-agent: Bytespider
Disallow: /
User-agent: CCBot
Disallow: /
User-agent: PerplexityBot
Disallow: /
User-agent: Applebot-Extended
Disallow: /
User-agent: *
Allow: /Dynamic robots.txt via API route (optional)
For environment-based rules (stricter in staging), use a src/routes/robots.txt.ts API route instead of the static file. Delete public/robots.txt first — static assets take precedence over route handlers in SolidStart.
// src/routes/robots.txt.ts
import { APIEvent } from "@solidjs/start/server";
export function GET(_event: APIEvent) {
const isProd = process.env.NODE_ENV === "production";
const rules = isProd
? `User-agent: GPTBot
Disallow: /
User-agent: ChatGPT-User
Disallow: /
User-agent: ClaudeBot
Disallow: /
User-agent: Google-Extended
Disallow: /
User-agent: Bytespider
Disallow: /
User-agent: CCBot
Disallow: /
User-agent: PerplexityBot
Disallow: /
User-agent: *
Allow: /`
: "User-agent: *
Disallow: /"; // Block all bots in staging
return new Response(rules, {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
}2. Hard 403 blocking via middleware
createMiddleware runs on every request before routing — the correct place for bot blocking. Module-level regex is compiled once at startup, not per-request. Always exempt /robots.txt first.
src/middleware.ts
import { createMiddleware } from "@solidjs/start/middleware";
// 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|FacebookBot|YouBot|PerplexityBot/i;
export default createMiddleware({
onRequest: [
(event) => {
const url = new URL(event.request.url);
// Always serve robots.txt — never block it
if (url.pathname === "/robots.txt") return;
const ua = event.request.headers.get("user-agent") ?? "";
if (BLOCKED_UAS.test(ua)) {
return new Response("Forbidden", {
status: 403,
headers: {
"Content-Type": "text/plain",
"X-Robots-Tag": "noai, noimageai",
},
});
}
},
],
});app.config.ts — register the middleware
// app.config.ts
import { defineConfig } from "@solidjs/start/config";
export default defineConfig({
middleware: "./src/middleware.ts",
// other options...
});One middleware file per app
SolidStart only supports a single middleware entry point. Put all your middleware logic (bot blocking, auth checks, logging) in one file and compose them using the onRequest array — each function in the array runs in order. Return a Response from any handler to short-circuit the chain.
Adding X-Robots-Tag to all responses
// src/middleware.ts — full version with X-Robots-Tag
import { createMiddleware } from "@solidjs/start/middleware";
const BLOCKED_UAS = /GPTBot|ChatGPT-User|OAI-SearchBot|ClaudeBot|Claude-Web|anthropic-ai|Google-Extended|Bytespider|CCBot|PerplexityBot|Applebot-Extended/i;
export default createMiddleware({
onRequest: [
// Bot blocking
(event) => {
const url = new URL(event.request.url);
if (url.pathname === "/robots.txt") return;
const ua = event.request.headers.get("user-agent") ?? "";
if (BLOCKED_UAS.test(ua)) {
return new Response("Forbidden", {
status: 403,
headers: { "Content-Type": "text/plain" },
});
}
},
],
onBeforeResponse: [
// Add X-Robots-Tag to all HTML responses
(_event, response) => {
const ct = response.headers.get("content-type") ?? "";
if (ct.includes("text/html")) {
response.headers.set("X-Robots-Tag", "noai, noimageai");
}
},
],
});3. noai meta tags — @solidjs/meta
SolidStart uses @solidjs/meta for head management. With SSR enabled (the default), <Meta> tags are included in the server-rendered HTML — non-JS crawlers see them on the initial response.
SPA mode warning
If ssr: false in app.config.ts, SolidStart produces a pure SPA — HTML shell is empty and meta tags are JavaScript-only. Non-JS crawlers will not see them. Use SSR mode (the default) for reliable meta tag delivery.
src/app.tsx — global noai meta tag
// src/app.tsx
import { MetaProvider, Meta, Title } from "@solidjs/meta";
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Suspense } from "solid-js";
import "./app.css";
export default function App() {
return (
<Router
root={(props) => (
<MetaProvider>
{/* Global noai tag — applies to every page */}
<Meta name="robots" content="noai, noimageai" />
<Suspense>{props.children}</Suspense>
</MetaProvider>
)}
>
<FileRoutes />
</Router>
);
}Per-page override
To allow AI training on specific pages (e.g., your public blog), override the global meta tag in the route component. @solidjs/meta deduplicates by name — the last declaration wins.
// src/routes/blog/[slug].tsx — allow training on public posts
import { Meta, Title } from "@solidjs/meta";
import { useParams } from "@solidjs/router";
import { createResource } from "solid-js";
export default function BlogPost() {
const params = useParams();
const [post] = createResource(() => fetchPost(params.slug));
return (
<>
{/* Override global noai — allow training on this public post */}
<Meta name="robots" content="index, follow" />
<Title>{post()?.title ?? "Blog"}</Title>
{/* ... */}
</>
);
}
// To block AI training on specific pages, omit the override — global noai applies.
// To block AI training on all EXCEPT specific pages, set global to "noai, noimageai"
// and override to "index, follow" only on the pages you want AI to train on.4. SSR vs SSG vs SPA — what bots see
The rendering mode determines what AI crawlers see. Middleware runs in SSR and at the edge/CDN layer for SSG. SPA mode has no server — bots see an empty HTML shell.
| Mode | Middleware runs? | Meta tags visible to bots? | Config |
|---|---|---|---|
| SSR (default) | ✓ on every request | ✓ server-rendered | default |
| SSG (prerender) | At edge/CDN layer | ✓ in static HTML | prerender: { crawlLinks: true } |
| SPA (ssr: false) | ✗ no server | ✗ JS-only (invisible to crawlers) | ssr: false |
SSG + prerendering: edge middleware for hard blocking
If you prerender pages to static HTML, your src/middleware.ts does not run per-request. Add hard blocking at the hosting platform instead:
// app.config.ts — SSG with prerendering
import { defineConfig } from "@solidjs/start/config";
export default defineConfig({
// middleware.ts still applies during SSR (dev) but not in static output
middleware: "./src/middleware.ts",
server: {
prerender: {
crawlLinks: true,
routes: ["/", "/about", "/blog"],
},
},
});# netlify.toml — hard block at CDN edge for prerendered SolidStart
[[edge_functions]]
path = "/*"
function = "block-ai-bots"
# netlify/edge-functions/block-ai-bots.ts
export default async (request: Request) => {
const url = new URL(request.url);
if (url.pathname === "/robots.txt") return;
const ua = request.headers.get("user-agent") ?? "";
const blocked = /GPTBot|ChatGPT-User|ClaudeBot|Google-Extended|Bytespider|CCBot|PerplexityBot/i;
if (blocked.test(ua)) {
return new Response("Forbidden", { status: 403 });
}
};5. X-Robots-Tag via hosting platform
For SSR deployments, the middleware approach above handles X-Robots-Tag. For static/SSG deployments, set it via your hosting platform config.
Vercel — vercel.json
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "X-Robots-Tag",
"value": "noai, noimageai"
}
]
}
]
}Netlify — netlify.toml
[[headers]]
for = "/*"
[headers.values]
X-Robots-Tag = "noai, noimageai"Cloudflare Pages — _headers file
# public/_headers (or dist/_headers for prerendered builds)
/*
X-Robots-Tag: noai, noimageai6. Deployment
SolidStart uses Vinxi presets for different deployment targets. The server.preset in app.config.ts determines the output format.
app.config.ts — Vercel preset
import { defineConfig } from "@solidjs/start/config";
export default defineConfig({
middleware: "./src/middleware.ts",
server: {
preset: "vercel", // or "netlify", "cloudflare-pages", "node", "bun", "deno"
},
});
// Build: npx vinxi build
// Vercel deploys automatically on git push (auto-detects SolidStart)
// Netlify: add build command "npx vinxi build" and publish dir ".output/public"Node.js — self-hosted
// app.config.ts
import { defineConfig } from "@solidjs/start/config";
export default defineConfig({
middleware: "./src/middleware.ts",
server: {
preset: "node",
},
});
// Build and run:
// npx vinxi build
// node .output/server/index.mjsDocker — multi-stage Node build
# Dockerfile
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npx vinxi build
FROM node:22-alpine AS runner
WORKDIR /app
COPY --from=builder /app/.output ./
EXPOSE 3000
CMD ["node", "server/index.mjs"]nginx reverse proxy
If you put nginx in front of SolidStart, you can add a second layer of bot blocking at the proxy level — runs before SolidStart even sees the request.
# /etc/nginx/sites-available/solidstart
map $http_user_agent $blocked_bot {
default 0;
"~*GPTBot" 1;
"~*ChatGPT-User" 1;
"~*ClaudeBot" 1;
"~*Claude-Web" 1;
"~*anthropic-ai" 1;
"~*Google-Extended" 1;
"~*Bytespider" 1;
"~*CCBot" 1;
"~*PerplexityBot" 1;
}
server {
listen 80;
server_name example.com;
# Never block robots.txt
location = /robots.txt {
proxy_pass http://localhost:3000;
}
location / {
if ($blocked_bot) {
return 403 "Forbidden";
}
proxy_pass http://localhost:3000;
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;
}
}| Platform | Preset | Middleware runs? | Notes |
|---|---|---|---|
| Vercel | "vercel" | ✓ Edge Function | Auto-detect, no config needed |
| Netlify | "netlify" | ✓ Edge Function | Build cmd: npx vinxi build |
| Cloudflare Pages | "cloudflare-pages" | ✓ CF Worker | Fastest edge runtime |
| Node.js / VPS | "node" | ✓ per request | Add nginx in front |
| Bun | "bun" | ✓ per request | Bun.serve() compatible |
| Static (SSG) | "static" | ✗ build-time only | Add platform edge rules |
Frequently asked questions
How do I serve robots.txt in SolidStart?
Place robots.txt in public/. Vinxi copies everything in public/ verbatim to the build output and serves it at the root URL — no configuration needed. For dynamic environment-based rules, use a src/routes/robots.txt.ts API route and delete the static file.
How do I add bot-blocking middleware in SolidStart?
Create src/middleware.ts, export a createMiddleware function, and register it in app.config.ts with middleware: "./src/middleware.ts". Compile your User-Agent regex at module scope. Exempt /robots.txt before testing the UA. Return new Response("Forbidden", { status: 403 }) to short-circuit.
Do AI bots see noai meta tags on SolidStart pages?
Yes, in SSR and SSG modes. SolidStart server-renders pages by default — <Meta> tags from @solidjs/meta are in the initial HTML. In SPA mode (ssr: false), meta tags are JavaScript-only and invisible to non-JS crawlers.
What is the difference between SSR, SSG, and SPA mode for bot blocking?
SSR (default): middleware runs per-request, full control. SSG (prerender): pages are static HTML — middleware still runs at edge/CDN but not during the build. SPA (ssr: false): no server, middleware not available, meta tags invisible to crawlers. For reliable bot blocking, use SSR mode or add platform-level edge rules for SSG.
How do I add X-Robots-Tag headers in SolidStart?
In the onBeforeResponse handler of your middleware, call response.headers.set("X-Robots-Tag", "noai, noimageai"). For SSG deployments, set it via vercel.json, netlify.toml, or a _headers file.
Can I use SolidStart API routes to serve a dynamic robots.txt?
Yes. Create src/routes/robots.txt.ts with a GET handler returning a plain text Response. Important: delete public/robots.txt first — static assets take precedence over route handlers and the API route will be silently ignored otherwise.
Is your site protected from AI bots?
Run a free scan to check your robots.txt, meta tags, and overall AI readiness score.