Skip to content

How to Block AI Bots in Go Buffalo

Buffalo is a Rails-inspired full-stack web framework for Go, combining routing, middleware, and asset handling in a single cohesive package. Bot blocking uses Buffalo's app.Use() middleware registration — the same pattern as every other Buffalo middleware, with access to Buffalo's context for request inspection and response rendering.

1. Bot pattern list

Define patterns in the actions package so they are accessible from both middleware and route handlers. strings.Contains with strings.ToLower is all that's needed — no regex, no external dependencies.

// actions/ai_bots.go
package actions

import "strings"

var aiBotPatterns = []string{
	"gptbot",
	"chatgpt-user",
	"claudebot",
	"anthropic-ai",
	"ccbot",
	"google-extended",
	"cohere-ai",
	"meta-externalagent",
	"bytespider",
	"omgili",
	"diffbot",
	"imagesiftbot",
	"magpie-crawler",
	"amazonbot",
	"dataprovider",
	"netcraft",
}

func isAiBot(userAgent string) bool {
	ua := strings.ToLower(userAgent)
	for _, pattern := range aiBotPatterns {
		if strings.Contains(ua, pattern) {
			return true
		}
	}
	return false
}

2. BotBlocker middleware

Buffalo middleware follows the func(next buffalo.Handler) buffalo.Handler pattern, where buffalo.Handler is func(c buffalo.Context) error. Set X-Robots-Tag at the top of the function — before any branch — so it appears on both blocked and passing responses without duplication.

// actions/middleware.go
package actions

import (
	"net/http"

	"github.com/gobuffalo/buffalo"
)

// BotBlocker sets X-Robots-Tag on every response and returns 403
// for known AI crawlers. Set the header BEFORE calling next(c) or
// returning so it appears on both blocked and passing responses.
func BotBlocker(next buffalo.Handler) buffalo.Handler {
	return func(c buffalo.Context) error {
		// Set header first — applies to every response path below
		c.Response().Header().Set("X-Robots-Tag", "noai, noimageai")

		// Allow robots.txt regardless of User-Agent
		if c.Request().URL.Path == "/robots.txt" {
			return next(c)
		}

		ua := c.Request().Header.Get("User-Agent")
		if isAiBot(ua) {
			return c.Render(http.StatusForbidden, r.String("Forbidden"))
		}

		return next(c)
	}
}

3. Register with app.Use()

Add app.Use(BotBlocker) in the App() function in app.go. Middleware runs in registration order. Place BotBlocker after Buffalo's built-in middleware (RequestID, ParameterLogger) but before your routes.

// app.go — register middleware with app.Use()
package actions

import (
	"github.com/gobuffalo/buffalo"
	"github.com/gobuffalo/buffalo/middleware"
	"github.com/gobuffalo/buffalo/middleware/csrf"
)

var app *buffalo.App

func App() *buffalo.App {
	if app == nil {
		app = buffalo.New(buffalo.Options{
			Env:         ENV,
			SessionName: "_myapp_session",
		})

		// Built-in middleware
		app.Use(middleware.RequestID)
		app.Use(middleware.ParameterLogger)
		app.Use(csrf.New)

		// AI bot blocker — runs before all routes
		app.Use(BotBlocker)

		// Routes
		app.GET("/", HomeHandler)
		app.GET("/about", AboutHandler)

		// public/ is served automatically — robots.txt bypass
		// handled by the path check in BotBlocker above
	}

	return app
}

4. public/robots.txt

Buffalo automatically serves the public/ directory. Because this goes through the same middleware stack, the path check c.Request().URL.Path == "/robots.txt" in BotBlocker ensures the file is always reachable regardless of User-Agent.

# public/robots.txt
# Buffalo serves public/ automatically through its asset pipeline.
# BotBlocker bypasses detection for /robots.txt path.

User-agent: *
Allow: /

User-agent: GPTBot
Disallow: /

User-agent: ClaudeBot
Disallow: /

User-agent: CCBot
Disallow: /

User-agent: Google-Extended
Disallow: /

5. Scoped middleware with route groups

Buffalo supports route groups via app.Group(). Apply BotBlocker to a group instead of the whole app when some routes (health checks, webhooks) should remain open to all callers.

// Scoped middleware — apply BotBlocker to specific route groups only
func App() *buffalo.App {
	if app == nil {
		app = buffalo.New(buffalo.Options{Env: ENV})

		// Public routes — no bot blocking
		app.GET("/robots.txt", RobotsHandler)
		app.GET("/health",     HealthHandler)

		// Protected routes — bot blocking applied to this group only
		protected := app.Group("/")
		protected.Use(BotBlocker)
		protected.GET("/",        HomeHandler)
		protected.GET("/blog",    BlogHandler)
		protected.GET("/api/v1/", APIHandler)
	}
	return app
}

Key points

Framework comparison — Go ecosystem

FrameworkMiddleware signatureShort-circuitRegister
Buffalofunc(next buffalo.Handler) buffalo.Handlerc.Render(403, r.String(...))app.Use()
Gingin.HandlerFuncc.AbortWithStatus(403)r.Use()
Echoecho.MiddlewareFuncc.String(403, "Forbidden")e.Use()
Chifunc(http.Handler) http.Handlerhttp.Error(w, ..., 403)r.Use()
Fiberfiber.Handlerc.Status(403).SendString(...)app.Use()

Buffalo's middleware signature is conceptually the same as Go stdlib (func(http.Handler) http.Handler) but wraps the context object as buffalo.Context for richer request inspection and Buffalo-specific rendering. The key difference from Gin/Echo: Buffalo uses c.Render() (which invokes the render engine) rather than a direct write.

Dependencies

No additional packages needed beyond Buffalo itself. All pattern matching uses Go stdlib strings. Install Buffalo with the CLI:

# Install Buffalo CLI
go install github.com/gobuffalo/cli/cmd/buffalo@latest

# Generate a new app
buffalo new myapp --db-type sqlite3

# Run in development
buffalo dev