# SEO metadata, sitemap and canonical URLs with the Next.js App Router SDK

> Recipe: set the page title, the description, the Open Graph image, the canonical URL and the robots rules from Uniform. Make a sitemap from the project map.

Source: https://docs.uniform.app/docs/sdk/nextjs-app-router/recipes/seo

**Goal:** Uniform authors set the SEO data of each page. Next.js writes it into the `<head>` of the page. A sitemap lists the pages of the project map.

You will make these files:

| File | Job |
| --- | --- |
| `lib/uniform/getRoute.ts` | Resolves the route one time for each request. |
| `lib/uniform/getUniformMetadata.ts` | Changes the composition parameters into a Next.js `Metadata` object. |
| `app/uniform/[code]/page.tsx` | Adds `generateMetadata` to the composition route. |
| `app/sitemap.ts` | Makes `/sitemap.xml` from the project map. |

## Prerequisites

- An app with the composition route `app/uniform/[code]/page.tsx`. Refer to [Add the composition route](https://docs.uniform.app/docs/sdk/nextjs-app-router/tutorial#step-10-add-the-composition-route).
- The package `@uniformdev/canvas`, for the constant `CANVAS_PUBLISHED_STATE` and the helper `isAssetParamValue`.
- These parameters on the composition type of your pages, for example `page`:

  | Parameter ID | Type |
  | --- | --- |
  | `metaTitle` | Text |
  | `metaDescription` | Text |
  | `ogImage` | Asset |
- The environment variable `NEXT_PUBLIC_SITE_URL` with the public URL of the site, for example `https://www.example.com`. The code uses it for the canonical URLs and for the sitemap.

## Step 1: Resolve the route one time for each request

`generateMetadata` and the page both need the route. Wrap the resolver in React `cache`. Then the two calls in one request share one result:

`lib/uniform/getRoute.ts`

```ts
import { resolveRouteFromCode } from "@uniformdev/next-app-router";
import { cache } from "react";

// One Route API call for each code in a request.
// generateMetadata and the page share the result.
export const getRoute = cache((code: string) => resolveRouteFromCode({ code }));
```

The function takes the code as a string. React `cache` compares the arguments by identity, so a new object on each call does not find the cached result.

## Step 2: Change the parameters into metadata

`lib/uniform/getUniformMetadata.ts`

```ts
import { CANVAS_PUBLISHED_STATE, isAssetParamValue } from "@uniformdev/canvas";
import type { ResolvedComposition } from "@uniformdev/next-app-router";
import type { Metadata } from "next";

const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000";

export function getUniformMetadata({ route, pageState }: ResolvedComposition): Metadata {
  const parameters = route.compositionApiResponse.composition.parameters ?? {};

  const text = (name: string) => {
    const value = parameters[name]?.value;
    return typeof value === "string" && value ? value : undefined;
  };

  const image = parameters.ogImage?.value;
  const imageUrl = isAssetParamValue(image) ? image[0]?.fields.url.value : undefined;

  const title = text("metaTitle") ?? route.compositionApiResponse.composition._name;
  const description = text("metaDescription");
  // The route path without the query string, for example "/about".
  const path = pageState.routePath.split("?")[0];

  return {
    title,
    description,
    alternates: { canonical: new URL(path, siteUrl).toString() },
    openGraph: {
      title,
      description,
      images: imageUrl ? [{ url: imageUrl }] : undefined,
    },
    // Do not index draft or editor content.
    robots: pageState.compositionState === CANVAS_PUBLISHED_STATE ? undefined : { index: false, follow: false },
  };
}
```

- **Parameters:** the composition parameters are in `result.route.compositionApiResponse.composition.parameters`. Each parameter has a `value`. A parameter is `undefined` when the author did not fill it in.
- **Title:** the code uses `metaTitle`. When `metaTitle` is empty, it uses the name of the composition.
- **Open Graph image:** an asset parameter contains a list of assets. The code uses the URL of the first asset.
- **Canonical URL:** `alternates.canonical` adds `<link rel="canonical">`. The code uses the route path of the page state, without the query string.
- **Robots:** `pageState.compositionState` tells you the state of the content. For draft and editor content, the code adds `noindex, nofollow`. Only a browser with the draft mode cookie sees draft content, so this rule is an extra safety step.

## Step 3: Add generateMetadata to the composition route

`app/uniform/[code]/page.tsx`

```tsx
import {
  requireComposition,
  UniformComposition,
  type UniformPageParameters,
} from "@uniformdev/next-app-router";
import type { Metadata } from "next";
import { resolveComponent } from "@/components/resolveComponent";
import { getRoute } from "@/lib/uniform/getRoute";
import { getUniformMetadata } from "@/lib/uniform/getUniformMetadata";

export const generateStaticParams = async () => [];

export async function generateMetadata(props: UniformPageParameters): Promise<Metadata> {
  const { code } = await props.params;
  // Applies a Uniform redirect, or calls notFound() when there is no route.
  const result = requireComposition(await getRoute(code));
  return getUniformMetadata(result);
}

export default async function UniformPage(props: UniformPageParameters) {
  const { code } = await props.params;
  return (
    <UniformComposition
      code={code}
      resolveComponent={resolveComponent}
      resolveRoute={({ code }) => getRoute(code)}
    />
  );
}
```

- `params` is a promise in Next.js 16. Use `await` before you read `code`.
- The resolver result can be a composition, a redirect or no route. `requireComposition` returns the composition. For a redirect, it calls `redirect()` or `permanentRedirect()`. For no route, it calls `notFound()`. Next.js permits these functions in `generateMetadata`.
- `resolveRoute` tells `UniformComposition` to use the same cached function. Thus the page and `generateMetadata` use one result.

## Step 4: Make a sitemap from the project map

`app/sitemap.ts`

```ts
import { getProjectMapClient } from "@uniformdev/next-app-router";
import type { MetadataRoute } from "next";

const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const { nodes } = await getProjectMapClient({
    // Get the project map again after one hour.
    cache: { type: "revalidate", interval: 3600 },
  }).getNodes({});

  return (nodes ?? [])
    // Keep nodes that have a composition.
    .filter((node) => node.type === "composition" && node.compositionId)
    // Skip dynamic nodes, for example /products/:slug.
    .filter((node) => !node.path.includes(":") && !node.path.includes("*"))
    .map((node) => ({ url: new URL(node.path, siteUrl).toString() }));
}
```

- The project map client sends no cache tags. Thus a Uniform webhook does not update the sitemap. The `revalidate` cache mode gets the project map again after the interval.
- A dynamic node, for example `/products/:slug`, is a pattern for many URLs. The sitemap cannot list them from the project map. Add these URLs from your own data source.
- The example gets all nodes in one request. For a large project map, use the `limit` and `offset` options of `getNodes`.
- Make sure that the middleware does not process `/sitemap.xml`. The matcher in [Add the middleware](https://docs.uniform.app/docs/sdk/nextjs-app-router/tutorial#step-9-add-the-middleware) excludes `sitemap.xml` and `robots.txt`.

## How it works

1. The middleware writes the route path and the composition state into the code.
2. `generateMetadata` resolves the route from the code. React `cache` keeps the result for the request.
3. The page renders `UniformComposition`, which gets the same result from `getRoute`.
4. Next.js adds the metadata tags to the `<head>` of the page.

The Uniform resolvers also use the Next.js data cache:

- **Published content:** the Route API request uses `force-cache`. The data cache keeps the response until a webhook revalidates its cache tags. Refer to [Caching](https://docs.uniform.app/docs/sdk/nextjs-app-router/caching#webhooks).
- **Draft content, editor content, and `next dev`:** the request uses `no-cache`. Each request goes to Uniform.

## Limits

- **Locales and path rewrites:** the canonical URL uses `pageState.routePath`. If `rewriteRequestPath` in the middleware changes the path, `routePath` is the project map path, not the public URL. Then make the canonical URL from your public URL.
- **Query strings:** the code removes the query string from the canonical URL. If a query string makes a different page, add it again.
- **Metadata after the first response:** Next.js can send `generateMetadata` results after the first part of the page. For bots that do not run JavaScript, Next.js waits for the metadata. Refer to the Next.js documentation for `generateMetadata`.
- **Ordinary Next.js routes:** for a page that resolves its own route, call `getUniformMetadata` only for a composition result. Refer to [Add Uniform regions to Next.js pages](https://docs.uniform.app/docs/sdk/nextjs-app-router/recipes/hybrid-pages).
