URL history and redirects
Every page keeps its last 20 URLs. Restore any of them from the tree, or turn the history into 301 redirects for your frontend.
When a page's slug changes, the plugin records the old one in the page's slugHistory field. Use it to restore a previous URL or to redirect old links to the new address.
History tracking
slugHistory holds the 20 most recent previous URLs, newest first. Older entries are dropped.
{
"slug": "marketing/about-us",
"slugHistory": [
{ "slug": "about-us", "changedAt": "2024-01-15T...", "reason": "move" },
{ "slug": "company/about", "changedAt": "2024-06-01T...", "reason": "edit-url" }
]
}Each entry records why the slug changed:
reason | Recorded when |
|---|---|
move | The page is moved with Update URLs, or its parent folder is moved with Update URLs and its slug cascades. |
edit-url | The page's segment is changed with Edit URL, or a folder's segment change cascades to the page. |
regenerate | URLs are rebuilt with Regenerate URLs. |
restore | A previous URL is restored. |
manual | Any other change, such as editing the folder or pageSegment on the page's own edit screen. |
rename | Not written by current versions. Kept so entries from before v0.3.13 stay valid. |
Restoring a previous URL
Right-click a page in the tree and choose URL History to see every previous URL with its time and reason, and restore any of them. Restoring sets the slug directly, removes that entry from the history and records the URL it replaced with reason restore.
Redirects
The plugin exports getRedirectMap(payload, collection, options?), which turns the history of every page in a collection into a list of { from, to } pairs with leading slashes. Call it on the server and redirect old URLs to their current page:
import { notFound, permanentRedirect } from 'next/navigation'
import { getPayload } from 'payload'
import config from '@payload-config'
import { getRedirectMap } from '@delmaredigital/payload-page-tree'
export default async function Page({ params }: { params: Promise<{ slug: string[] }> }) {
const { slug } = await params
const fullSlug = slug.join('/')
const payload = await getPayload({ config })
const { docs } = await payload.find({
collection: 'pages',
where: { slug: { equals: fullSlug } },
limit: 1,
})
if (!docs[0]) {
const redirects = await getRedirectMap(payload, 'pages')
const redirect = redirects.find((r) => r.from === `/${fullSlug}`)
if (redirect) permanentRedirect(redirect.to)
notFound()
}
// ...render docs[0]
}getRedirectMap reads every page that has history, so cache its result if your site has many pages. Called without options it reads with full access, which suits middleware and build steps. Pass { req } to apply that request user's read rules, or { overrideAccess: false } to apply your anonymous read rules.
The redirects endpoint
The same list is available over HTTP:
const res = await fetch('/api/page-tree/redirects?collection=pages')
const { redirects, count } = await res.json()
// {
// "redirects": [
// { "from": "/about-us", "to": "/marketing/about-us" },
// { "from": "/company/about", "to": "/marketing/about-us" }
// ],
// "count": 2
// }The endpoint needs an authenticated user
Like every tree endpoint, /api/page-tree/redirects passes through the access gate, which by default requires a logged-in user. An anonymous request — for example a fetch from Next.js middleware or proxy — gets 401. For public redirects, use getRedirectMap on the server instead.
collection is required and must be one of the plugin's configured collections; otherwise the endpoint answers 400.
URL helper endpoints
The Edit URL modal uses two read-only endpoints you can also call directly. Both pass through the same access gate.
| Endpoint | Purpose |
|---|---|
GET /api/page-tree/check-segment | Checks whether a proposed segment is free. Query: type (page or folder), segment, optional parentId and excludeId, and collection when type is page. Returns { available, slugifiedSegment }. |
GET /api/page-tree/folder-impact?folderId=X | Counts the pages, including those in nested subfolders, whose slugs would be rewritten if the folder's URL segment changed. Returns { childPageCount }. |

