Build a Headless Site with dotCMS
Handle Vanity Redirects
In this chapter you'll add vanity URL redirect handling to all three pages. It's a small addition — one function call per page — but it's the kind of thing that bites you in production if you skip it.
What vanity URLs are#
In dotCMS, content editors can create vanity URLs — short, memorable URLs that redirect to another page. For example, /promo → /products/summer-offer-2026. They're managed in dotAdmin without touching code.
When you fetch a page that has a vanity URL configured, dotCMS includes a vanityUrl object in the page response:
{
"vanityUrl": {
"action": 301,
"forwardTo": "/products/summer-offer-2026"
}
}If your frontend ignores this, the redirect never happens — the user lands on the wrong page with no error. You need to check for it and call Next.js redirect() before rendering anything.
Add the utility to seo.ts#
The handleVanityRedirect function belongs in src/utils/seo.ts alongside the other utilities you added in Chapter 7. Add it at the end of the file:
export function handleVanityRedirect(
vanityUrl: { action?: number; forwardTo?: string } | undefined,
redirectFn: (url: string) => never,
): void {
if ((vanityUrl?.action ?? 0) > 200 && vanityUrl?.forwardTo) {
redirectFn(vanityUrl.forwardTo);
}
}action > 200 catches both 301 (permanent) and 302 (temporary) redirects. redirectFn is Next.js's redirect — passed in so the utility stays testable and framework-agnostic.
Update the catch-all page#
Open src/app/[[...slug]]/page.tsx. Import redirect from Next.js and handleVanityRedirect from your utilities, then call it right after the page fetch:
import { notFound, redirect } from "next/navigation";
import { handleVanityRedirect } from "@/utils/seo";
// ... other imports
export default async function CatchAllPage({ params }: PageProps) {
const { slug } = await params;
const path = resolvePath(slug);
const pageContent = await getDotCMSPage(path, {
content: { navigation: navigationQuery },
});
if (!pageContent) return notFound();
handleVanityRedirect(pageContent.pageAsset?.vanityUrl, redirect);
// ... rest of the function
}The call goes immediately after the notFound() check — before you extract nav items, build JSON-LD, or return any JSX.
Update the blog listing page#
Open src/app/blog/page.tsx and add the same two lines:
import { notFound, redirect } from "next/navigation";
import { handleVanityRedirect } from "@/utils/seo";
// ...
export default async function BlogPage() {
const pageContent = await getDotCMSPage(PATH, blogListGraphQL);
if (!pageContent) return notFound();
handleVanityRedirect(pageContent.pageAsset?.vanityUrl, redirect);
// ... rest of the function
}Update the blog detail page#
Open src/app/blog/[...slug]/page.tsx and do the same:
import { notFound, redirect } from "next/navigation";
import { handleVanityRedirect } from "@/utils/seo";
// ...
export default async function BlogDetailPage({ params }: PageProps) {
const { slug } = await params;
const path = `/blog/${slug.join("/")}`;
const pageContent = await getDotCMSPage(path, blogDetailGraphQL);
if (!pageContent) return notFound();
handleVanityRedirect(pageContent.pageAsset?.vanityUrl, redirect);
// ... rest of the function
}Try it#
To test this, go to dotAdmin and create a vanity URL:
- Go to Rules or Vanity URLs in the left sidebar
- Create a new vanity URL: forward
/test-redirectto/ - Open
http://localhost:3000/test-redirectin your browser - You should land on the home page
Checkpoint#
-
handleVanityRedirectadded toseo.ts - All three pages import and call it after the
notFound()check - A test vanity URL in dotAdmin redirects correctly in the browser
Next up
Chapter 10: What's Next