• Home
BuildWithMatija
Get In Touch
  1. Home
  2. Blog
  3. Next.js
  4. Dynamic robots.txt in Next.js for Multi-Tenant Sites

Dynamic robots.txt in Next.js for Multi-Tenant Sites

Serve per-tenant robots.txt, sitemap.xml, and humans.txt with Next.js App Router and Payload CMS—caching and…

9th January 2026·Updated on:22nd February 2026·MŽMatija Žiberna·
Next.js
Dynamic robots.txt in Next.js for Multi-Tenant Sites

⚡ Next.js Implementation Guides

In-depth Next.js guides covering App Router, RSC, ISR, and deployment. Get code examples, optimization checklists, and prompts to accelerate development.

No spam. Unsubscribe anytime.

Related Posts:

  • •Fix Next.js Prisma Serialization: Centralize Decimal/Date
  • •Ultimate Fiidbakk Next.js Integration: Quick Setup Guide
  • •Fix next-intl Redirects Breaking Locale Routing: Quick Guide

I recently tackled a common challenge in multi-tenant architectures: how to serve unique robots.txt and sitemap.xml files for different domains running on the same application. A static file in /public just doesn't cut it when Tenant A needs to block AI bots while Tenant B wants full indexing.

This guide walks through the robust, cached solution I implemented using Next.js App Router and Payload CMS.

1. The Core Utility: Centralized Tenant Lookups

The first step was to stop repeating ourselves. We needed a single, reliable way to resolve the current tenant from the hostname—whether it's a custom domain (example.com) or a subdomain (tenant.app.com).

I centralized this logic in src/payload/db/index.ts using unstable_cache to keep performance high. This function is the backbone of our SEO strategy.

// File: src/payload/db/index.ts

export const getTenantByDomain = async (domain: string) => {
  return await unstable_cache(
    async () => {
      const payload = await getPayloadClient();
      const tenants = await payload.find({
        collection: "tenants",
        where: {
          or: [
            { domain: { equals: domain } },
            { slug: { equals: domain.split('.')[0] } } // Fallback to slug for subdomain patterns
          ]
        },
        limit: 1,
      });
      return tenants.docs[0] || null;
    },
    [CACHE_KEY.TENANT_BY_DOMAIN(domain)],
    {
      tags: [TAGS.TENANTS],
      revalidate: 3600, // Revalidate every hour
    }
  )();
};

Why this matters: This function handles the heavy lifting of database queries and caching. By centralizing it, we ensure that robots.txt, sitemap.xml, and humans.txt all "agree" on which tenant is active.

2. Dynamic Robots.txt with AI Protection

With the tenant lookup in place, I created a dynamic route handler for robots.txt. This isn't just a static file anymore; it's code. This allows us to inject the correct sitemap URL for the specific tenant and apply global rules, like blocking AI scrapers.

// File: src/app/robots.ts

import type { MetadataRoute } from "next";
import { headers } from "next/headers";
import { getTenantByDomain } from "@/payload/db";

export default async function robots(): Promise<MetadataRoute.Robots> {
  // Get hostname from request headers
  const hostname = (await headers()).get('host') || 'www.adart.com';
  
  // Try to find tenant by domain or subdomain
  const tenant = await getTenantByDomain(hostname);
  
  // If no tenant found, use fallback (adart)
  const baseUrl = tenant?.domain ? \`https://\${tenant.domain}\` : \`https://\${hostname}\`;
  
  return {
    rules: [
      // Block AI Scraping Bots
      {
        userAgent: ["GPTBot", "CCBot", "Google-Extended"],
        disallow: ["/"],
      },
      // Standard bots
      {
        userAgent: "*",
        allow: "/",
        disallow: [
          "/admin",
          "/api",
        ],
        crawlDelay: 1,
      },
    ],
    sitemap: \`\${baseUrl}/sitemap.xml\`,
    host: baseUrl,
  };
}

Key Features:

  • Dynamic Host: The sitemap link automatically matches the visitor's domain.
  • AI Blocking: explicit blocks for GPTBot, CCBot, and Google-Extended protecting our content intelligence.

3. Dynamic Humans.txt

To give credit where it's due, I also implemented a humans.txt endpoint. This is a nice touch that adds personality and transparency to the site, dynamically acknowledging the specific tenant.

// File: src/app/humans.ts

import { headers } from "next/headers";
import { getTenantByDomain } from "@/payload/db";

export default async function humans() {
  const hostname = (await headers()).get('host') || '';
  const tenant = await getTenantByDomain(hostname);
  const tenantName = tenant?.name || 'Ad Art';
  
  const content = \`/* TEAM */
  
  Site built by: Ad Art Team
  For: \${tenantName}
  
/* SITE */
  
  Standards: HTML5, CSS3, TypeScript
  Components: Payload CMS, Next.js\`;

  return new Response(content, {
    headers: { 'Content-Type': 'text/plain; charset=utf-8' },
  });
}

4. The Critical Fix: Middleware Matcher

This was the tricky part. Even with the files in place, robots.txt was returning a 404.

The culprit was src/middleware.ts. The matcher regex was swallowing requests to files if they didn't match specific patterns. I updated the negative lookahead to explicitly exclude any path with a file extension (like .txt or .xml).

// File: src/middleware.ts

export const config = {
  matcher: [
    /*
     * Match all request paths except for:
     * ...
     * 5. Static files (e.g. /favicon.ico, /robots.txt) - Matched by .*\\..*
     */
    '/((?!api|_next|_static|_vercel|.*\\..*).*)',
  ],
};

The Lesson: If your middleware runs on file routes, it might try to rewrite them to tenant paths (e.g., /tenant-slugs/.../robots.txt), which don't exist. Excluding files from middleware ensures they hit the App Router handlers directly.

Conclusion

By moving away from static files and leveraging Next.js Route Handlers, we've created a SEO infrastructure that allows:

  1. Automatic Sitemaps per tenant.
  2. Smart Indexing Rules that protect against AI scraping.
  3. Zero Maintenance when onboarding new tenants.

Let me know if you have questions!

Thanks, Matija

📄View markdown version
0

Frequently Asked Questions

Comments

Leave a Comment

Your email will not be published

Stay updated! Get our weekly digest with the latest learnings on NextJS, React, AI, and web development tips delivered straight to your inbox.

10-2000 characters

• Comments are automatically approved and will appear immediately

• Your name and email will be saved for future comments

• Be respectful and constructive in your feedback

• No spam, self-promotion, or off-topic content

Matija Žiberna
Matija Žiberna
Full-stack developer, co-founder

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.

You might be interested in

Fix Next.js Prisma Serialization: Centralize Decimal/Date
Fix Next.js Prisma Serialization: Centralize Decimal/Date

19th January 2026

Ultimate Fiidbakk Next.js Integration: Quick Setup Guide
Ultimate Fiidbakk Next.js Integration: Quick Setup Guide

22nd January 2026

Fix next-intl Redirects Breaking Locale Routing: Quick Guide
Fix next-intl Redirects Breaking Locale Routing: Quick Guide

23rd January 2026

Table of Contents

  • 1. The Core Utility: Centralized Tenant Lookups
  • 2. Dynamic Robots.txt with AI Protection
  • 3. Dynamic Humans.txt
  • 4. The Critical Fix: Middleware Matcher
  • Conclusion
On this page:
  • 1. The Core Utility: Centralized Tenant Lookups
  • 2. Dynamic Robots.txt with AI Protection
  • 3. Dynamic Humans.txt
  • 4. The Critical Fix: Middleware Matcher
  • Conclusion
Build With Matija Logo

Build with Matija

Matija Žiberna

I turn scattered business knowledge into one usable system. End-to-end system architecture, AI integration, and development.

Quick Links

Payload CMS Websites
  • Bespoke AI Applications
  • Projects
  • How I Work
  • Blog
  • Get in Touch

    Have a project in mind? Let's discuss how we can help your business grow.

    Contact me →
    © 2026BuildWithMatija•Principal-led system architecture•All rights reserved