# Dynamic CMS routes

URL: https://developers.uidu.org/docs/recipes/dynamic-cms-routes

> Generate a static page for every CMS slug at build time with generateStaticParams.

<Callout type="warn" title="Legacy CMS">
  This recipe uses the legacy Projects CMS (`listPages` / `getPage`). For a Site, build the params from
  `listSitePages` — see step 6 of the [Public website](https://developers.uidu.org/docs/paths/public-website.md) path.
</Callout>

A `[slug]/page.tsx` route that pre-renders every CMS page in your project.

## The route

```tsx
// app/[slug]/page.tsx
import { getPage, listPages } from '@uidu/client';
import { PageBlocks } from '@uidu/react';
import { notFound } from 'next/navigation';
import { client } from '@/lib/uidu';
import { Hero, Feature, ContactForm } from '@/components/blocks';

export async function generateStaticParams() {
  const pages = await listPages(client, {
    projectId: process.env.UIDU_PROJECT_ID!,
    first: 200,
  });
  return pages.map((p) => ({ slug: p.slug }));
}

export default async function CmsPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const page = await getPage(client, {
    projectId: process.env.UIDU_PROJECT_ID!,
    slug,
  });

  if (!page) notFound();

  return (
    <PageBlocks
      pageBlocks={page.pageBlocks}
      components={{
        Hero,
        feature1: Feature,
        ContactForm,
      }}
    />
  );
}
```

## Why this works

* `listPages` returns every page slug in the project — that's what `generateStaticParams` needs.
* `getPage` is called once per slug at build time. Next.js caches the result per route.
* `<PageBlocks>` looks up each block's `templateBlock.shortname` in your `components` map and renders the matching component with the block's `fieldValues`.

## Add a sitemap

```tsx
// app/sitemap.ts
import { listPages } from '@uidu/client';
import { client } from '@/lib/uidu';

export default async function sitemap() {
  const pages = await listPages(client, {
    projectId: process.env.UIDU_PROJECT_ID!,
    first: 500,
  });

  return pages.map((p) => ({
    url: `https://example.com/${p.slug}`,
    lastModified: p.updatedAt,
  }));
}
```

## Add ISR revalidation

If you want the page to refresh without a redeploy, set a revalidation interval:

```tsx
export const revalidate = 60; // seconds
```

For event-driven invalidation, configure a webhook in your uidu workspace to hit `/api/revalidate?path=/<slug>` and call `revalidatePath` from there.

<Callout type="info">
  For more than \~500 pages, walk every page with the [`paginate()`](https://developers.uidu.org/docs/recipes/paginate.md) helper.
</Callout>
