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