Skip to content
Guides/Medusa.js

How to Block AI Bots on Medusa.js: Complete 2026 Guide

Medusa.js v2 is an open-source headless commerce platform built on Express and Node.js. It exposes a REST API for your storefront — it does not render your frontend HTML. Bot blocking uses defineMiddlewares() in src/api/middlewares.ts (Layers 3–4) and a robots.txt in the static/ directory (Layer 1). The noai meta tag (Layer 2) belongs in your storefront, not in Medusa.

Medusa v2 architecture

/store/*Storefront API — product, cart, order routes. Primary AI scraping target.
/admin/*Admin API — internal dashboard API (port 9000 or proxied).
/hooks/*Webhook routes — payment providers, fulfillment callbacks.
/robots.txtServed from static/ — must remain accessible to all bots.
Your storefront (Next.js Starter, custom Nuxt, etc.) runs separately — add noai meta tags there.

Protection layers

1
robots.txtstatic/robots.txt — served from project root at /robots.txt
2
noai meta tagYour storefront (Next.js Starter / Nuxt / SvelteKit) — not in Medusa
3
X-Robots-Tag headerdefineMiddlewares() → Express middleware → res.set() after next()
4
Hard 403 blockdefineMiddlewares() → Express middleware → res.sendStatus(403)

Layer 1: robots.txt

Place robots.txt in the static/ directory at your Medusa project root. Medusa serves this directory at the web root automatically — no route definition required.

# static/robots.txt

User-agent: *
Allow: /

User-agent: GPTBot
User-agent: ClaudeBot
User-agent: anthropic-ai
User-agent: Google-Extended
User-agent: CCBot
User-agent: Bytespider
User-agent: Applebot-Extended
User-agent: PerplexityBot
User-agent: Diffbot
User-agent: cohere-ai
User-agent: FacebookBot
User-agent: omgili
User-agent: omgilibot
User-agent: Amazonbot
User-agent: DeepSeekBot
User-agent: MistralBot
User-agent: xAI-Bot
User-agent: AI2Bot
Disallow: /

Project structure: static/robots.txt lives alongside src/, medusa-config.ts, and package.json at the root of your Medusa project.

Layer 2: noai meta tag

Medusa is a headless backend — it returns JSON, not HTML. Add the noai meta tag in your storefront. The official Medusa Next.js Starter uses the App Router:

Next.js Starter (App Router)

// app/layout.tsx
export const metadata = {
  // ...existing metadata
  other: { robots: 'noai, noimageai' },
};

Nuxt storefront

// nuxt.config.ts
export default defineNuxtConfig({
  app: { head: { meta: [
    { name: 'robots', content: 'noai, noimageai' }
  ]}}
})

SvelteKit storefront

<!-- src/routes/+layout.svelte -->
<svelte:head>
  <meta name="robots" content="noai, noimageai">
</svelte:head>

Layers 3 & 4: defineMiddlewares()

Medusa v2 discovers middleware from a single file: src/api/middlewares.ts. The file must export a default function named defineMiddlewares() that returns { routes: [...] }. Each route entry has a matcher (string glob or RegExp) and a middlewares array of Express-compatible functions.

src/api/middlewares.ts

import type { MedusaNextFunction, MedusaRequest, MedusaResponse } from '@medusajs/medusa';

const AI_BOT_PATTERNS = [
  'gptbot', 'chatgpt-user', 'oai-searchbot',
  'claudebot', 'anthropic-ai', 'claude-web',
  'google-extended', 'ccbot', 'bytespider',
  'applebot-extended', 'perplexitybot', 'diffbot',
  'cohere-ai', 'facebookbot', 'meta-externalagent',
  'omgili', 'omgilibot', 'amazonbot',
  'deepseekbot', 'mistralbot', 'xai-bot', 'ai2-bot',
];

const EXEMPT_PATHS = new Set(['/robots.txt', '/sitemap.xml', '/favicon.ico']);

function aiBotBlocker(
  req: MedusaRequest,
  res: MedusaResponse,
  next: MedusaNextFunction
): void {
  const path = req.path.split('?')[0];

  // Always pass through exempt paths
  if (EXEMPT_PATHS.has(path)) {
    next();
    return;
  }

  const ua = (req.headers['user-agent'] || '').toLowerCase();

  // Layer 4: hard 403 block
  if (AI_BOT_PATTERNS.some(pattern => ua.includes(pattern))) {
    res.sendStatus(403);
    return;
  }

  // Layer 3: X-Robots-Tag — inject on all legitimate responses
  res.set('X-Robots-Tag', 'noai, noimageai');
  next();
}

export default defineMiddlewares({
  routes: [
    {
      matcher: '/**',
      middlewares: [aiBotBlocker],
    },
  ],
});

Key points

  • defineMiddlewares() is the only supported way to register middleware in Medusa v2. The file src/api/middlewares.ts is auto-discovered — no explicit registration step required.
  • matcher: '/**' covers all API routes including /store/*, /admin/*, and /hooks/*. Use /store/** to restrict blocking to storefront routes only.
  • req.path does not include the query string — no need to split on ?. Included here as a defensive guard in case of edge cases.
  • res.sendStatus(403) sends a minimal response and terminates the middleware chain — equivalent to res.status(403).send() but more concise. Do not call next() after blocking.
  • MedusaRequest, MedusaResponse, MedusaNextFunction extend the standard Express types — standard Express middleware functions work here without modification.

Storefront-only blocking

To block bots only on storefront API routes and leave admin and webhook routes unaffected:

export default defineMiddlewares({
  routes: [
    {
      matcher: '/store/**',
      middlewares: [aiBotBlocker],
    },
    // Webhooks: never block (payment providers use programmatic UA strings)
    // Admin: served separately — apply blocking at proxy/CDN level
  ],
});

Merging with existing middleware

If you already have a src/api/middlewares.ts (e.g. for CORS or authentication), add the bot blocker to the existing routes array. Order matters — middleware runs in array order. Place the bot blocker first so blocked requests never reach CORS or auth:

import { authenticate } from '@medusajs/medusa';
import type { MedusaNextFunction, MedusaRequest, MedusaResponse } from '@medusajs/medusa';
// ... aiBotBlocker function as above ...

export default defineMiddlewares({
  routes: [
    // 1. Bot blocking runs first — rejected requests never reach auth
    {
      matcher: '/store/**',
      middlewares: [aiBotBlocker],
    },
    // 2. Existing auth middleware for protected admin routes
    {
      matcher: '/admin/**',
      middlewares: [authenticate()],
    },
  ],
});

Verification

# Default Medusa backend port is 9000

# Layer 1 — robots.txt (must return 200 to all bots)
curl http://localhost:9000/robots.txt

# Layer 3 — X-Robots-Tag on legitimate store request
curl -I http://localhost:9000/store/products
# Expected: X-Robots-Tag: noai, noimageai

# Layer 4 — hard block on bot user-agent
curl -A "Mozilla/5.0 (compatible; GPTBot/1.0; +https://openai.com/gptbot)" \
  -I http://localhost:9000/store/products
# Expected: HTTP/1.1 403 Forbidden

# robots.txt must stay accessible even to bots
curl -A "GPTBot" -I http://localhost:9000/robots.txt
# Expected: HTTP/1.1 200 OK

FAQ

How do I add middleware in Medusa.js v2?

Create src/api/middlewares.ts and export a default call to defineMiddlewares(). The function takes an object with a routes array. Each entry needs a matcher (string glob or RegExp) and a middlewares array of Express-compatible functions. Medusa auto-discovers this file — no manual registration needed. This replaces the v1 loader pattern where you called app.use() directly.

Where does Medusa v2 serve static files like robots.txt?

Medusa serves the static/ directory from your project root at the web root. Place robots.txt at static/robots.txt and it is available at /robots.txt automatically. This directory is not compiled — files are served as-is at runtime. Other common static files (favicon.ico, sitemap.xml) follow the same convention.

Where do noai meta tags go in a Medusa-powered store?

In your storefront — not in Medusa. Medusa returns JSON from its REST API. Your storefront (the official Next.js Starter, or a custom Nuxt/SvelteKit frontend) renders the HTML. Add the noai meta tag to your storefront's base layout. For the Next.js Starter: add other: { robots: 'noai, noimageai' } to the metadata export in app/layout.tsx.

Will the middleware block the Medusa Admin dashboard?

The Admin dashboard (@medusajs/dashboard) is a separate Vite SPA that calls the /admin/* API routes on the backend. Using matcher: '/**' will apply bot blocking to /admin/* API calls. The admin SPA itself is served separately (default port 7001 in development) and is not affected by defineMiddlewares(). For production, if you serve admin and API from the same domain via a reverse proxy, apply bot blocking at the proxy level for admin static assets.

Does this work with Medusa v1?

No. Medusa v1 used a different registration mechanism: you created a loader file and called app.use() on the Express app directly. The defineMiddlewares() pattern is exclusive to Medusa v2 (v2.0+). The middleware function itself (aiBotBlocker) is identical — only the registration changes. For Medusa v1, create src/api/index.ts and export a loader that adds your middleware to the Express app.

Is your site protected from AI bots?

Run a free scan to check your robots.txt, meta tags, and overall AI readiness score.