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:

  1. Go to Rules or Vanity URLs in the left sidebar
  2. Create a new vanity URL: forward /test-redirect to /
  3. Open http://localhost:3000/test-redirect in your browser
  4. You should land on the home page

Checkpoint#


  • handleVanityRedirect added to seo.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

Continue →