BuildWithMatija
  1. Home
  2. Blog
  3. Payload
  4. Payload Redirects Plugin: Fix 301s in Multi-Tenant Sites

Payload Redirects Plugin: Fix 301s in Multi-Tenant Sites

Configure Payload redirects for multi-tenant, bilingual sites — fix uniqueness with compound indexes and middleware.

18th August 2026·Updated on:19th August 2026··
Payload
Payload Redirects Plugin: Fix 301s in Multi-Tenant Sites

Evaluating Payload CMS Implementation Costs?

Scope design, content structure, and migration hours to estimate a realistic production timeline and hosting setup.

Try the Cost EstimatorGet a Second Opinion

📚 Comprehensive Payload CMS Guides

Detailed Payload guides with field configuration examples, custom components, and workflow optimization tips to speed up your CMS development process.

No spam. Unsubscribe anytime.

📄View markdown version
0

Frequently Asked Questions

About the author

Matija Žiberna

Matija Žiberna

Full-stack developer, co-founder

AboutResume

Self-taught full-stack developer sharing lessons from building software and startups.

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.

Contents

  • What the Redirects Plugin Actually Adds
  • Installing and Configuring the Plugin
  • Fixing the Uniqueness Constraint for Multi-Tenant, Multi-Locale Sites
  • Wiring Redirects Into the Multi-Tenant Plugin
  • Comparing Redirect Approaches
  • FAQ
  • Conclusion
On this page:
  • What the Redirects Plugin Actually Adds
  • Installing and Configuring the Plugin
  • Fixing the Uniqueness Constraint for Multi-Tenant, Multi-Locale Sites
  • Wiring Redirects Into the Multi-Tenant Plugin
  • Comparing Redirect Approaches
Build with Matija logo

Build with Matija

Senior-led B2B websites, applications, content systems, and digital infrastructure. Business-first, full-stack, AI-assisted, no handoffs.

Services

  • B2B Website Development
  • CMS Architecture Review & Platform Blueprint
  • Next.js + Payload Advisory
  • AI Integration & Implementation

Resources

  • CMS Hub
  • B2B Website Strategy
  • E-commerce Hub
  • Blog
  • Case Studies

Payload CMS

  • Payload CMS Developer
  • Payload CMS Migration
  • Payload CMS Demos
  • All Payload CMS Resources

Discuss your project

Planning a rebuild, migration, application, workflow change, or platform decision? Start with the business problem and the system behind it.

Book a discovery callContact me →
© 2026Build with Matija•All rights reserved•Privacy Policy•Terms of Service
BuildWithMatija
Get In Touch

The @payloadcms/plugin-redirects package injects a full redirects collection into Payload, letting editors manage HTTP redirect rules from the Admin Panel instead of hardcoding them into next.config.js or an Nginx rewrite file. I added it during a WordPress-to-Payload migration where the legacy site carried 31 explicit 301 rules that had to survive the move without breaking search rankings. Tested with Payload 3.88.0 and the plugin at the matching version, in a multi-tenant project with bilingual content across en-CA and fr-CA.

The site being migrated had accumulated years of vanity URLs and merged taxonomy paths, tracked in WordPress through a legacy SEO plugin's redirect table. Moving that table's logic into Payload meant more than copying rows across. The project's existing redirects collection had been hand-built with plain source and destination text fields, which meant a destination page's slug could change and silently break every redirect pointing at it, with no relationship to catch the drift. The official plugin replaces that with a real relationship field, so a redirect can point at a living document instead of a hardcoded string. It also defaults to a single unique constraint on the source path, which conflicts directly with a multi-tenant, multi-locale project where the same relative path is legitimately reused across sites and languages. This guide covers the plugin's structure, the exact configuration used to fix that constraint, and how it fits alongside the multi-tenant plugin in the same config.

If you're setting up the multi-tenant foundation first, my production-ready multi-tenant setup guide covers the tenant isolation this plugin needs to sit on top of.

What the Redirects Plugin Actually Adds

@payloadcms/plugin-redirects is a first-party Payload package published in the same monorepo and version line as core, alongside plugin-nested-docs, plugin-seo, plugin-form-builder, and plugin-multi-tenant. Installing 3.88.0 alongside Payload 3.88.0 keeps it on the compatibility line it was built against.

Registering the plugin appends an entire new collection to the config, defaulting to the slug redirects. That collection ships with:

  • A from field (text, indexed, unique by default) holding the incoming path to match.
  • A to group field with a radio toggle between reference and custom. Choosing reference exposes a relationship picker scoped to whichever collections you list; choosing custom exposes a plain URL text field.
  • A type select field for the HTTP status code, populated from redirectTypes if you pass that option: 301, 302, 303, 307, or 308.
  • Built-in admin translations for English, French, and Spanish.

The plugin does not execute redirects at the HTTP layer on its own. It's a data store. Actually running redirects at request time is still the frontend's job, typically through Next.js middleware or an edge proxy that queries this collection.

Installing and Configuring the Plugin

bash
pnpm add @payloadcms/plugin-redirects
typescript
// File: src/payload/config/plugins/redirects.ts
export const redirects: Plugin = redirectsPlugin({
  collections: [...REDIRECT_TARGET_COLLECTIONS],
  redirectTypes: ["301", "302", "307", "308"],
  redirectTypeFieldOverride: {
    defaultValue: "308",
  },
  overrides: {
    admin: {
      group: "Site configuration",
      defaultColumns: ["from", "to.type", "locale", "site"],
    },
    access: {
      create: createContentCreateAccess("redirects"),
      delete: createContentDeleteAccess("redirects"),
      read: createContentReadAccess("redirects"),
      update: createContentUpdateAccess("redirects"),
    },
    indexes: [{ fields: ["site", "locale", "from"], unique: true }],
    fields: ({ defaultFields }) => {
      const fields = defaultFields.map((field) => {
        if ("name" in field && field.name === "from") {
          return {
            ...field,
            unique: false,
          };
        }
        return field;
      });

      return [
        {
          name: "locale",
          type: "select",
          required: true,
          options: [...SUPPORTED_LOCALES],
        },
        ...fields,
      ];
    },
  },
});

collections limits which document types are selectable as redirect targets, in this project spanning ten collections including pages, blogs, recipes, and products. redirectTypes enables the status code dropdown, defaulted here to 308 since that preserves the original request method during migration-driven redirects rather than forcing every redirect to a GET. overrides.access wires the project's own RBAC functions into the collection instead of leaving it on Payload's default access rules. overrides.indexes and the fields override are what fix the uniqueness problem covered next.

Register the plugin before the multi-tenant plugin in the plugin array:

typescript
// File: src/payload/config/plugins/index.ts
export const plugins: Plugin[] = [
  ecommerce,
  nestedDocs,
  seo,
  redirects,
  formBuilder,
  multiTenant,
  importExport,
  mcp,
];

Order matters because redirects generates a brand-new collection at config time, and multiTenant needs to discover that collection to inject the site relationship into it. Registering multiTenant first means it runs before the redirects collection exists.

Fixing the Uniqueness Constraint for Multi-Tenant, Multi-Locale Sites

The plugin's default from field carries unique: true at the field level. That's a reasonable default for a single-site, single-locale project, where the same source path should only ever map to one destination. It breaks immediately on a multi-tenant, bilingual project, where /about is a legitimate redirect source on Site A, Site B, and again separately in the fr-CA locale. A single-field unique constraint has no way to know those are three different contexts.

The fix does two things: it strips the field-level uniqueness from from, and it replaces it with a compound index across site, locale, and from.

typescript
indexes: [{ fields: ["site", "locale", "from"], unique: true }],
fields: ({ defaultFields }) => {
  const fields = defaultFields.map((field) => {
    if ("name" in field && field.name === "from") {
      return {
        ...field,
        unique: false,
      };
    }
    return field;
  });

  return [
    {
      name: "locale",
      type: "select",
      required: true,
      options: [...SUPPORTED_LOCALES],
    },
    ...fields,
  ];
},

defaultFields gives you the plugin's own generated field array to map over, rather than replacing the whole set from scratch. Finding the from field by name and spreading it with unique: false removes the conflicting constraint. The compound index then enforces the uniqueness the project actually needs: one redirect rule per source path, per site, per locale, rather than one redirect rule per source path globally. The locale field itself isn't part of the plugin's default schema; it gets pushed into the returned array explicitly, since a bilingual redirects table needs to know which language a given rule applies to.

Wiring Redirects Into the Multi-Tenant Plugin

With the collection generated and the constraint fixed, the multi-tenant plugin needs to know it should scope this collection to tenants:

typescript
// File: src/payload/config/plugins/multi-tenant.ts
collections: {
  [Blogs.slug]: {},
  [Pages.slug]: {},
  redirects: {},
  [Stories.slug]: {},
  // ...remaining collections
},

Because redirects is a plugin-generated collection with no static class or exported slug constant to reference, it gets registered by its literal string slug rather than an imported reference. Adding it to this map is what causes the multi-tenant plugin to auto-inject the site relationship field, which is the field the compound index above depends on.

Comparing Redirect Approaches

ApproachWhen to useTrade-off
@payloadcms/plugin-redirectsEditorially managed redirects that need to stay in sync with living CMS contentRequires a frontend layer (middleware or edge proxy) to actually query and execute the redirect; adds nothing at the HTTP level by itself
Hand-rolled redirects collection with plain text fieldsVery small, static redirect sets that rarely changeNo relational binding to real documents, so a destination slug change breaks the redirect silently, and any relational or status-code logic has to be built and maintained by hand
Hardcoded next.config.js redirects arrayA handful of permanent, code-level rulesEvery new rule needs a code change and a deployment, which puts redirect management outside marketing or content editors' reach
Edge or reverse proxy rewrites (Nginx, Cloudflare)High-volume, performance-critical rewrite rulesFully disconnected from CMS content, unmanageable by non-engineering staff, and invisible to editors who need to add a rule

The migration case for this project ruled out the first two alternatives directly: the legacy hand-rolled collection had already shown its weakness by breaking silently on slug changes, and 31 rules were too many to justify hardcoding into the Next.js config given ongoing editorial ownership after launch.

FAQ

Does the redirects plugin actually perform the HTTP redirect? No. The plugin only provides the data collection and the admin UI for managing rules. Executing a redirect at request time is the frontend's responsibility, typically through Next.js middleware or an edge proxy that queries this collection and issues the response.

Why does the default from field break on a multi-tenant project? The plugin sets unique: true directly on the from field, which enforces global uniqueness across the entire collection. A multi-tenant or multi-locale project legitimately reuses the same source path across different sites or languages, which the field-level constraint has no way to distinguish. Replacing it with a compound index on site, locale, and from fixes this.

Can a redirect point to a document instead of a hardcoded URL? Yes. The to group field exposes a radio choice between reference, which opens a relationship picker scoped to whichever collections you list in the plugin config, and custom, which takes a plain URL string. Using reference means the redirect stays valid even if the target document's slug changes later.

Which HTTP status codes does the plugin support? 301, 302, 303, 307, and 308, exposed as a select field when you pass redirectTypes into the plugin config. Setting a defaultValue on that field controls what new redirect rules default to.

Where does the plugin need to sit relative to the multi-tenant plugin? Before it. plugin-redirects generates the collection at config-evaluation time, and the multi-tenant plugin needs that collection to already exist so it can inject the site field into it. Registering multi-tenant first means the redirects collection isn't there yet to scope.

Conclusion

Migrating a legacy site's redirect rules into Payload is straightforward once the collection exists, but the plugin's defaults assume a single-site, single-locale project. A multi-tenant, bilingual site needs the uniqueness constraint rebuilt as a compound index rather than a single field, and the plugin's defaultFields override makes that a small, contained change rather than a fork. Combined with correct plugin ordering against the multi-tenant plugin, the result is an editorially managed redirects table that preserved all 31 legacy rules from the WordPress migration without a single hardcoded rewrite rule in the codebase.

Let me know in the comments if you have questions, and subscribe for more practical development guides.

Thanks, Matija