Skip to content

Redirects ​

What you'll learn

Send visitors (and search engines) from old addresses to new ones: redirects editors manage in the admin, and automatic ones when a page's address changes.

Before this page: Plugins.

Redirects under Settings, one open in a panel

When a page moves, its old address should answer with a 301 to the new one: visitors and old links keep working, and search engines move the page's ranking to the new address instead of dropping it. @easy-cms/plugin-redirects adds a Redirects list under Settings in the admin, and adds a redirect by itself when a published post's slug changes.

bash
npm install @easy-cms/plugin-redirects
bash
pnpm add @easy-cms/plugin-redirects
bash
yarn add @easy-cms/plugin-redirects
bash
bun add @easy-cms/plugin-redirects
ts
import { redirectsPlugin } from '@easy-cms/plugin-redirects'

// One function for a post's address, shared with the SEO plugin's generateURL.
const postURL = (doc) => (doc.slug ? `/posts/${doc.slug}` : null)

export default defineConfig({
  // …
  plugins: [
    redirectsPlugin({
      collections: ['posts'], // redirects may point to posts, and follow them
      url: ({ doc }) => postURL(doc),
    }),
  ],
})

The plugin adds a redirects collection: create a migration for it (easy-cms migrate:create redirects, see Migrations).

In the admin ​

Each redirect has:

  • From: the old path, e.g. /old-page. The query string and a trailing slash are ignored.
  • To (address): a path on the site or a full URL, or a document from collections. A redirect to a document always goes to its current address, even after it moves again.
  • Type: 301 (moved for good, the default), 302 or 307 (for now), 308 (for good, keeping the method).

Redirects open in a drawer over the list, like other small collections. With the MCP plugin, an assistant can manage them too, when its API key allows.

When an address changes ​

With collections and url, a published document whose address changes gets a redirect from the old address to the document, in every locale:

  • Only live pages: saving a draft, or renaming a page that was never published, adds nothing.
  • No chains: the redirect points to the document, so after a second rename both old addresses go straight to the newest one.
  • No loops: renaming a page back to an old address removes the redirect from that address.

Turn it off with autoRedirect: false, or give a list of collections.

Serve them ​

resolveRedirect(cms, url) returns { location, status } for a path, or null. Redirects are read once and kept in memory, so it is cheap to call on every request. Changes apply at once on the server that made them; other servers (serverless instances) catch up within cacheTTL (60 seconds by default). The visitor's query string is kept.

ts
import { resolveRedirect } from '@easy-cms/plugin-redirects'

export default defineEventHandler(async (event) => {
  const url = getRequestURL(event)
  if (/^\/(_nuxt|api|admin)(\/|$)/.test(url.pathname)) return
  const redirect = await resolveRedirect(await useEasyCMS(), url)
  if (redirect) return sendRedirect(event, redirect.location, redirect.status)
})
ts
import { getEasyCMS } from '@easy-cms/next'
import { resolveRedirect } from '@easy-cms/plugin-redirects'
import { type NextRequest, NextResponse } from 'next/server'
import cmsConfig from './easy-cms.config'

// The proxy (formerly middleware) runs on Node.js, so it can use the Local API.
export async function proxy(request: NextRequest) {
  const redirect = await resolveRedirect(await getEasyCMS(cmsConfig), request.nextUrl)
  if (redirect)
    return NextResponse.redirect(new URL(redirect.location, request.url), redirect.status)
}

export const config = { matcher: ['/((?!api/|admin|_next/|favicon.ico).*)'] }
ts
// GET <routes.api>/resolve-redirect?path=/old-page
// 200 { "location": "/new-page", "status": 301 }, or 404
const response = await fetch(`${CMS}/api/cms/resolve-redirect?path=${encodeURIComponent(path)}`)
if (response.ok) {
  const { location, status } = await response.json()
  // send the redirect with your framework or host
}

On Next.js 15, middleware runs on the Edge runtime by default, where the Local API doesn't run: set export const config = { runtime: 'nodejs', matcher: [...] } (Next.js 15.5 and later), or look the path up in your not-found page and call permanentRedirect().

Options ​

OptionDefault
collections[]Collections a redirect can point to; their pages get automatic redirects.
url—({ collection, doc, locale }) => address: a document's address. Needed with collections.
autoRedirecttrueAdd redirects when a published page's address changes; false, or a list of collections.
slug'redirects'Slug of the redirects collection.
cacheTTL60000How long other servers keep redirects in memory, in ms.

Next steps ​

  • SEO: the sitemap lists pages at their new addresses.
  • Configuration: put your own collections under Settings with admin: { group: 'settings' }.

Released under the MIT License.