Install Ghost Shield
Identify, block, and analyze AI bot traffic in your application.
✨ Designed for Next.js (v13+)
Edge Caching
Rules are fetched once and cached in memory. Zero latency impact.
Fail Open
If our API goes down, your site stays up. Security never breaks UX.
Async Logs
Analytics are sent after the response. No blocking the user.
Get your API Key (Optional)
Required for Analytics and Custom Rules. Not required for basic protection.
Configure Environment
Add your key to your .env.local file.
GHOST_API_KEY=sk_ghost_...Install the Shield
Create a new file at lib/shield.ts and paste the following code.
This is a zero-dependency module designed for Next.js Edge Runtime.
/* lib/shield.ts */
import { NextRequest, NextResponse } from 'next/server';
// Configuration Interface
interface ShieldConfig {
ghostApiUrl: string;
apiKey?: string;
allow?: string[]; // Whitelist slugs e.g. ['googlebot']
debug?: boolean;
}
interface ShieldRules {
blocked: {
user_agents: string[];
ips: string[];
};
}
// In-Memory Cache (Edge Friendly)
let cachedRules: ShieldRules | null = null;
let lastFetch = 0;
const CACHE_TTL = 3600 * 1000; // 1 Hour
export class GhostShield {
private config: ShieldConfig;
constructor(apiKey?: string, apiUrl = "https://ghost.tech/api/v1/rules") {
this.config = { apiKey, ghostApiUrl: apiUrl };
}
async inspect(request: NextRequest): Promise<{ isBlocked: boolean }> {
try {
const rules = await this.getRules();
if (!rules) return { isBlocked: false }; // Fail Open
const ua = request.headers.get('user-agent') || '';
const ip = request.headers.get('x-forwarded-for') || '';
let isBlocked = false;
// 1. Check User Agent
if (this.checkUserAgent(ua, rules.blocked.user_agents)) {
// Check Allow List (Whitelist)
const isAllowed = this.config.allow?.some(slug =>
ua.toLowerCase().includes(slug.toLowerCase())
);
if (!isAllowed) {
isBlocked = true;
}
}
// 2. Check IP (Simple CIDR or Exact Match)
if (!isBlocked && this.checkIp(ip, rules.blocked.ips)) {
isBlocked = true;
}
// 3. Logistics
if (this.config.apiKey) {
// Fire and Forget Log
this.logRequest({
apiKey: this.config.apiKey,
userAgent: ua,
path: request.nextUrl.pathname,
verdict: isBlocked ? 'blocked' : 'allowed'
}).catch(console.error);
}
return { isBlocked };
} catch (err) {
console.error('[Ghost] Shield Error:', err);
return { isBlocked: false }; // Fail Open
}
}
private async getRules(): Promise<ShieldRules | null> {
const now = Date.now();
if (cachedRules && (now - lastFetch < CACHE_TTL)) return cachedRules;
try {
const res = await fetch(this.config.ghostApiUrl, {
headers: { 'User-Agent': 'Ghost-Shield/1.0' },
signal: AbortSignal.timeout(1500),
});
if (res.ok) {
cachedRules = await res.json();
lastFetch = now;
return cachedRules;
}
} catch (e) { /* Ignore fetch errors */ }
return cachedRules;
}
private checkUserAgent(ua: string, patterns: string[]): boolean {
return patterns.some(pattern => ua.includes(pattern));
}
private checkIp(ip: string, list: string[]): boolean {
return list.includes(ip);
}
private async logRequest(data: any) {
// Use string concatenation for safety in docs
const endpoint = new URL(this.config.ghostApiUrl).origin + '/api/v1/log';
await fetch(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
}Activate Middleware
Using Next.js 16?
In Next.js 16+, middleware.ts is deprecated. Create proxy.ts instead and export proxy instead of middleware.
Import and use the shield in your middleware.ts (or proxy.ts).
import { NextRequest, NextResponse } from 'next/server';
import { GhostShield } from '@/lib/shield';
const shield = new GhostShield(process.env.GHOST_API_KEY);
// For Next.js 16+: export async function proxy(req: NextRequest)
export async function middleware(req: NextRequest) {
// 1. Inspect the request
const decision = await shield.inspect(req);
// 2. Act on the decision
if (decision.isBlocked) {
return new NextResponse('Access Denied by Ghost Shield', { status: 403 });
}
// 3. Continue
return NextResponse.next();
}Verify Protection
Deploy your app. Then, simulate a bot request to test the shield.
curl -A "GPTBot" https://your-site.comYou should receive a 403 Forbidden response. Check your Ghost Dashboard to see the analytics appearing in real-time.
