# Redirects (/docs/guides/redirects)

When a page gets another path, its old path becomes a **redirect** to the page. The delivery API answers the old path with where the page is now, so your site sends visitors and search engines on with a `301` instead of a 404. Content records the redirects; your site answers them.

## Where they come from [#where-they-come-from]

- **Moving a page or folder.** **Move** in the entry's sidebar (a new slug, or another folder) keeps the old path of the page and of every page below it. See [moving](/docs/guides/publishing#moving-and-redirects).
- **Changing a slug in one locale.** Slugs are [per locale](/docs/guides/locales#urls-per-locale), and so are redirects: a new Dutch slug keeps the old Dutch path, the English one doesn't change.
- **Changing locales.** A new default locale or another fallback can change the paths of locales without their own slug; the paths that change become redirects.
- **By hand.** For an old path no page here ever had, such as the URLs of a site you import from another content system. See [adding one by hand](#adding-one-by-hand).

## How they behave [#how-they-behave]

- **A redirect follows its page.** One recorded by a move points at the entry, not at a path: moving the page again changes where all its old paths go, without chains. One [added by hand](#adding-one-by-hand) points at a fixed path or URL, and stays there when a page moves or is deleted.
- **A page placed on an old path takes it over.** Creating or moving a page onto a path that was a redirect removes the redirect, in that locale.
- **Deleting the page removes the redirects its moves recorded.** Its old paths are 404s from then on. Restoring it from the trash doesn't bring them back.
- **Only published pages count on a live site.** A delivery token doesn't see redirects to entries that aren't published; a preview token does. Redirects made by hand are always visible.
- **Every environment has its own.** Redirects are recorded where the page moved and aren't [promoted](/docs/guides/environments#promoting). A page moved in staging and promoted to production gets its new path there without a redirect from the old one: move published pages in production, or add the redirect there by hand.

## Managing them [#managing-them]

**Settings → Preview and site → Redirects** lists every old path with where it goes now, per locale (one row when all locales go to the same page), and the page's name. **Delete** removes one: its old path is a 404 from then on. Editors and above can delete them.

The [activity log](/docs/guides/activity) records redirects added by hand and deleted ones. Moving sends the `entry.moved` [webhook](/docs/guides/webhooks) with the previous paths.

## Adding one by hand [#adding-one-by-hand]

The dashboard doesn't add redirects; the [agent API](/docs/api/agents), [MCP](/docs/getting-started/agents) and [management tokens](/docs/api/management) with the `content` scope do. Use them when importing a site, for its old URLs, in production too (MCP's `createRedirect`, `listRedirects` and `deleteRedirect`):

```http
POST https://content.obelism.studio/api/agent/organizations/{org}/spaces/{space}/{environment}/redirects/create
Authorization: Bearer cms_m_…
Content-Type: application/json

{ "from": "/news/2019/our-new-office", "to": "/blog/our-new-office" }
```

| Field    | Value                                                                                                               |
| -------- | ------------------------------------------------------------------------------------------------------------------- |
| `from`   | The old path, with or without a leading or trailing `/`; no spaces, `?` or `#`, at most 2048 characters             |
| `to`     | A path in this space, with or without a leading `/`, or an absolute `http(s)` URL (`https://shop.example.com/sale`) |
| `locale` | Optional. Without it, the redirect is added in every locale                                                         |

A path a page has in that locale, or one that already redirects there, answers `409`: delete the old redirect first to change it. The target path isn't checked against your pages: it can be a route of your site that isn't in Content. `GET …/redirects` lists them with their entry, and `POST …/redirects/delete` with `{ "from", "locale"? }` removes one.

## On your site [#on-your-site]

There are two ways to answer them, and most sites use the first.

### When a page isn't found [#when-a-page-isnt-found]

Look the path up with `findPage` instead of `getPage`. It answers `{ entry }` for a page, `{ redirect: { path } }` for an old path, and `null` for neither:

```tsx title="src/app/[[...slug]]/page.tsx"
import { notFound, permanentRedirect } from 'next/navigation'

export default async function CatchAll({ params }: Props) {
	const { slug } = await params
	const found = await content.findPage<PageEntry>(slug?.join('/') || 'home', {
		depth: 2,
	})
	if (found?.redirect) {
		const to = found.redirect.path
		permanentRedirect(/^https?:\/\//.test(to) ? to : `/${to}`)
	}
	if (!found) notFound()

	return <Page entry={found.entry} />
}
```

Paths come without a leading `/` and without the locale. With several locales, pass `locale` to `findPage` and put it back where your routes have it (`/${locale}/${to}`). A `to` that starts with `http` is an absolute URL someone added by hand; send it as it is. Without the SDK, `GET /api/delivery/v1/pages?path=…` answers `404` with `redirect: { path }` (see [moved pages](/docs/api/delivery#moved-pages)).

This costs nothing extra for pages that exist, and a redirect works the moment a page moves.

### As rules built ahead of time [#as-rules-built-ahead-of-time]

To answer old paths before your app runs (at the CDN, or in `next.config`), list them all with `getAllRedirects` and turn them into your host's rules:

```ts title="next.config.ts"
import { ContentClient } from '@obelism/content'
import type { NextConfig } from 'next'

const content = new ContentClient({ token: process.env.CONTENT_TOKEN! })

const config: NextConfig = {
	redirects: async () => {
		const rules = []
		for await (const { from, to } of content.getAllRedirects({ locale: 'en' }))
			rules.push({
				source: `/${from}`,
				destination: /^https?:\/\//.test(to) ? to : `/${to}`,
				permanent: true,
			})
		return rules
	},
}

export default config
```

Each item is `{ locale, from, to }`; leave `locale` out for every locale's. `GET /api/delivery/v1/redirects` answers up to 1000 at a time, with `limit` and `skip`. Rules like these are only as new as your last build: rebuild on the `entry.moved` webhook, or keep `findPage` in the catch-all route as well, so a page moved since still redirects.
