Guides · Publishing
Redirects
Old paths keep working. Where redirects come from, how they behave per locale and environment, adding one by hand for an import, and answering them on your site with a 301 at request time or as rules built ahead of time.
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
- 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.
- Changing a slug in one locale. Slugs are 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.
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 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. 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
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 records redirects added by hand and deleted ones. Moving sends the entry.moved webhook with the previous paths.
Adding one by hand
The dashboard doesn't add redirects; the agent API, MCP and management tokens with the content scope do. Use them when importing a site, for its old URLs, in production too (MCP's createRedirect, listRedirects and deleteRedirect):
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
There are two ways to answer them, and most sites use the first.
When a page isn't 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:
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).
This costs nothing extra for pages that exist, and a redirect works the moment a page moves.
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:
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 configEach 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.