How to Block AI Bots on Fresh (Deno)
Fresh is Deno's official web framework. It uses Preact for rendering, island architecture for partial hydration (only interactive components ship JavaScript), and file-based routing under routes/. Fresh runs a server at runtime — unlike static site generators, you have full middleware control over every request. This makes AI bot blocking straightforward: robots.txt, noai meta tags in layouts, response headers via middleware, and hard 403 rejection — all without relying on hosting platform features.
1. robots.txt
Fresh serves all files in the static/ directory at the root path. Place your robots.txt here for the simplest approach.
Static robots.txt
Create static/robots.txt:
# Block all 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: /
# Allow legitimate search crawlers
User-agent: Googlebot
Allow: /
User-agent: Bingbot
Allow: /
User-agent: *
Allow: /Fresh serves this directly from static/ — no build step required, no configuration needed. The file is available at yoursite.com/robots.txt immediately.
Dynamic robots.txt via route handler
For environment-aware content (e.g., blocking all crawlers on staging), create a route handler. Fresh route handlers take priority over static files at the same path.
Create routes/robots.txt.ts:
import { Handlers } from "$fresh/server.ts";
export const handler: Handlers = {
GET(_req) {
const isProduction = Deno.env.get("DENO_DEPLOYMENT_ID") !== undefined;
if (!isProduction) {
return new Response(
"# Staging — block all crawlers\nUser-agent: *\nDisallow: /\n",
{ headers: { "content-type": "text/plain; charset=utf-8" } },
);
}
const robotsTxt = `# 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: /
# Allow search crawlers
User-agent: Googlebot
Allow: /
User-agent: Bingbot
Allow: /
User-agent: *
Allow: /
`;
return new Response(robotsTxt, {
headers: { "content-type": "text/plain; charset=utf-8" },
});
},
};routes/robots.txt.ts and static/robots.txt exist, the route handler wins. Remove the static file to avoid confusion, or keep the static version as a fallback and rely on the route handler for dynamic logic.2. noai meta tags in Preact layouts
The noai and noimageai meta values signal to AI crawlers that page content and images should not be used for training. Add them to your app layout so every page is covered.
App-wide layout
Fresh uses routes/_app.tsx as the root layout for all pages. This is where you define the <html>, <head>, and <body> structure:
// routes/_app.tsx
import { type PageProps } from "$fresh/server.ts";
export default function App({ Component }: PageProps) {
return (
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{/* AI bot protection — applies to all pages */}
<meta name="robots" content="noai, noimageai" />
<title>My Fresh Site</title>
</head>
<body>
<Component />
</body>
</html>
);
}Per-route override with Head component
To allow individual pages to override the robots meta, use the <Head> component from $fresh/runtime.ts:
// routes/blog/[slug].tsx
import { Head } from "$fresh/runtime.ts";
import { type PageProps } from "$fresh/server.ts";
export default function BlogPost({ data }: PageProps) {
return (
<>
<Head>
{/* Override: allow indexing for this page */}
<meta name="robots" content="index, follow" />
<title>{data.title}</title>
</Head>
<article>
<h1>{data.title}</h1>
{/* ... */}
</article>
</>
);
}<Head> component, Fresh injects those tags into the <head> of the response. If both _app.tsx and a route define a robots meta tag, both will appear in the HTML. Browsers use the last matching meta tag, so the per-route override works — but validate with View Source to confirm the tag order is correct.Data-driven meta from handler
For pages where the robots value comes from a database or CMS, use a route handler to pass data to the component:
// routes/page/[id].tsx
import { Head } from "$fresh/runtime.ts";
import { type Handlers, type PageProps } from "$fresh/server.ts";
interface PageData {
title: string;
robots: string;
content: string;
}
export const handler: Handlers<PageData> = {
async GET(_req, ctx) {
const page = await fetchPage(ctx.params.id);
if (!page) return ctx.renderNotFound();
return ctx.render(page);
},
};
export default function DynamicPage({ data }: PageProps<PageData>) {
return (
<>
<Head>
<meta name="robots" content={data.robots || "noai, noimageai"} />
<title>{data.title}</title>
</Head>
<article dangerouslySetInnerHTML={{ __html: data.content }} />
</>
);
}3. X-Robots-Tag via middleware
HTTP response headers are the most reliable signal for crawlers that ignore meta tags. Fresh middleware runs server-side on every request — ideal for setting headers globally.
Global middleware
Create routes/_middleware.ts:
// routes/_middleware.ts
import { FreshContext } from "$fresh/server.ts";
export async function handler(
req: Request,
ctx: FreshContext,
): Promise<Response> {
const resp = await ctx.next();
resp.headers.set("X-Robots-Tag", "noai, noimageai");
return resp;
}This adds the X-Robots-Tag header to every response — HTML pages, API routes, and static file requests that pass through the Fresh server.
_middleware.ts file applies to all routes in its directory and all subdirectories. Place it in routes/ for global coverage, or in routes/blog/ to only affect blog routes.Selective headers — exclude API routes
If you have API routes that should not receive the header (e.g., JSON endpoints consumed by your own frontend):
// routes/_middleware.ts
import { FreshContext } from "$fresh/server.ts";
export async function handler(
req: Request,
ctx: FreshContext,
): Promise<Response> {
const resp = await ctx.next();
// Only add header to HTML responses
const contentType = resp.headers.get("content-type") || "";
if (contentType.includes("text/html")) {
resp.headers.set("X-Robots-Tag", "noai, noimageai");
}
return resp;
}4. Hard 403 via middleware
For aggressive blocking — reject known AI bot requests before they reach your route handlers. This is the most effective layer because the bot receives no content at all.
// routes/_middleware.ts
import { FreshContext } from "$fresh/server.ts";
const AI_BOT_PATTERNS = [
/GPTBot/i,
/ClaudeBot/i,
/Claude-Web/i,
/anthropic-ai/i,
/CCBot/i,
/Google-Extended/i,
/PerplexityBot/i,
/Applebot-Extended/i,
/Amazonbot/i,
/meta-externalagent/i,
/Bytespider/i,
/Diffbot/i,
/YouBot/i,
/cohere-ai/i,
];
function isAIBot(userAgent: string): boolean {
return AI_BOT_PATTERNS.some((pattern) => pattern.test(userAgent));
}
export async function handler(
req: Request,
ctx: FreshContext,
): Promise<Response> {
const ua = req.headers.get("user-agent") || "";
if (isAIBot(ua)) {
return new Response("Forbidden", { status: 403 });
}
const resp = await ctx.next();
resp.headers.set("X-Robots-Tag", "noai, noimageai");
return resp;
}ctx.next(). Legitimate requests continue through the chain and receive the header on the response.Middleware chain order
Fresh supports multiple middleware files. A routes/_middleware.ts runs before routes/blog/_middleware.ts, which runs before the route handler. Place the bot-blocking middleware at the root level (routes/_middleware.ts) so it intercepts all requests before any other middleware or handler executes.
Logging blocked requests
To monitor how many AI bots are hitting your site, add logging before the 403 response:
if (isAIBot(ua)) {
console.log(`[blocked] ${new Date().toISOString()} ${ua} ${req.url}`);
return new Response("Forbidden", { status: 403 });
}On Deno Deploy, these logs appear in the project dashboard under Logs. Locally, they print to the terminal running deno task start.
5. Islands and client-side considerations
Fresh's island architecture means most of your page is server-rendered HTML with zero JavaScript. Only components in the islands/ directory ship JavaScript to the client. This has implications for AI bot protection:
- Meta tags work reliably. Because pages are server-rendered, the
noaimeta tag is present in the initial HTML response — no JavaScript execution needed. AI crawlers that don't execute JavaScript will still see the meta tag. - Islands don't affect crawling. Interactive islands hydrate on the client, but their content is already rendered in the server HTML. AI bots see the same content regardless of JavaScript support.
- No client-side bot detection needed. Since Fresh has a server runtime, all bot detection happens in middleware — before any HTML is generated. Client-side UA checks are unnecessary and unreliable.
6. Deno Deploy configuration
Deno Deploy is the primary deployment target for Fresh. All middleware, route handlers, and static file serving work identically — no adapter or special configuration needed.
Deploy setup
- Push your Fresh project to GitHub
- Connect the repository in the Deno Deploy dashboard (
dash.deno.com) - Set the entry point to
main.ts - Deploy — middleware runs on every request at the edge
Environment variables
If your robots.txt route handler checks for environment variables, set them in the Deno Deploy dashboard under Settings → Environment Variables. The DENO_DEPLOYMENT_ID variable is set automatically on every deployment.
Custom headers via deployctl
If deploying via deployctl (CI/CD), the same middleware applies. No additional header configuration is needed at the platform level — Fresh middleware handles everything:
# .github/workflows/deploy.yml
- name: Deploy to Deno Deploy
uses: denoland/deployctl@v1
with:
project: my-fresh-site
entrypoint: main.ts7. Deployment comparison
Fresh runs a Deno server — it needs a runtime environment, not just static file hosting. Here's how AI bot protection features work across deployment targets:
| Platform | robots.txt | Meta tags | X-Robots-Tag | Hard 403 |
|---|---|---|---|---|
| Deno Deploy | ✓ | ✓ | ✓ | ✓ |
| Docker / self-hosted | ✓ | ✓ | ✓ | ✓ |
| Fly.io | ✓ | ✓ | ✓ | ✓ |
| Railway | ✓ | ✓ | ✓ | ✓ |
| AWS Lambda | ✓ | ✓ | ✓ | ✓ |
Because Fresh includes a runtime server, all four protection layers work on every platform that can run Deno. There is no “static hosting” limitation like with SSGs — middleware executes on every request regardless of the host.
FAQ
How do I add robots.txt to a Fresh project?
Place robots.txt in the static/ directory. Fresh serves all files in static/ at the root path automatically — no configuration needed. For dynamic content (environment-specific rules), create routes/robots.txt.ts with a GET handler that returns a new Response() with text/plain content type.
How do I add noai meta tags in Fresh?
In routes/_app.tsx, add <meta name="robots" content="noai, noimageai" /> inside the <head> element. This applies to every page. For per-route overrides, use the <Head> component from $fresh/runtime.ts in individual route components.
How does Fresh middleware work for AI bot blocking?
Create routes/_middleware.ts and export an async handler function. The function receives the Request and a FreshContext with ctx.next() to continue the middleware chain. For headers: call ctx.next(), get the response, set headers, return it. For blocking: check the User-Agent and return new Response("Forbidden", { status: 403 }) before calling ctx.next() — the route handler never executes.
Does Fresh have a build step that affects robots.txt?
Fresh 1.x had no build step — files in static/ were served directly at runtime. Fresh 2.x added an optional build step (deno task build), but static/ files are still served from the directory directly in both modes. Your robots.txt works without any build configuration in either version.
How is Fresh different from Lume for AI bot blocking?
Lume is a static site generator — it outputs HTML files with no runtime server. AI bot blocking in Lume depends on hosting platform features (Netlify Edge Functions, Vercel middleware, etc.). Fresh has a runtime server with built-in middleware, so all blocking happens in your application code. You don't need platform-specific configuration — the same _middleware.ts works on Deno Deploy, Docker, Fly.io, or any Deno runtime.
Will blocking AI bots affect my SEO?
Blocking AI-specific crawlers (GPTBot, ClaudeBot, CCBot, Google-Extended) does not affect standard search engine indexing. Googlebot and Bingbot are separate user agents from Google-Extended and are not blocked by the configurations in this guide. Always include explicit Allow rules for Googlebot and Bingbot in your robots.txt to make your intent unambiguous.
Is your site protected from AI bots?
Run a free scan to check your robots.txt, meta tags, and overall AI readiness score.