# Project Map Client SDK

> How to use the Uniform Project Map Client to query site structure, resolve locale paths, and build navigation.

Source: https://docs.uniform.app/docs/sdk/project-map-client

The Project Map Client (`ProjectMapClient` from `@uniformdev/project-map`) provides programmatic access to Uniform's project map -- the hierarchical site structure that maps URL paths to compositions. Use it for building navigation menus, generating sitemaps, resolving locale-specific paths, and working with site structure data.

---

## Installation

```bash
npm install @uniformdev/project-map
```

---

## Initializing the client

### Standalone usage

```ts
import { ProjectMapClient } from "@uniformdev/project-map";

const projectMapClient = new ProjectMapClient({
  apiKey: process.env.UNIFORM_API_KEY,
  apiHost: process.env.UNIFORM_CLI_BASE_URL || "https://uniform.global",
  projectId: process.env.UNIFORM_PROJECT_ID,
});
```

### Within the App Router SDK

The SDK provides a pre-configured client via `getProjectMapClient`:

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

const projectMapClient = getProjectMapClient({
  cache: { type: "force-cache" },
});
```

---

## Getting project map definitions

A Uniform project can have multiple project maps. Retrieve the available definitions:

```ts
const definitions = await projectMapClient.getProjectMapDefinitions();

for (const pm of definitions.projectMaps) {
  console.log(pm.id, pm.name);
}
```

Get a specific definition by ID:

```ts
const definition = await projectMapClient.getProjectMapDefinition({
  projectMapId: "your-project-map-id",
});
```

---

## Getting nodes

### Flat node list

`getNodes` returns project map nodes in a flat list format. This is useful for sitemaps, breadcrumbs, or any scenario where you need a flat list of pages:

```ts
const response = await projectMapClient.getNodes({
  projectMapId: "your-project-map-id",
});

for (const node of response.nodes ?? []) {
  console.log(node.path, node.name, node.type);
}
```

> **Use an AI coding assistant:**
>
> The [`uniform-breadcrumbs`](https://github.com/uniformdev/agent-skills/tree/main/skills/uniform-breadcrumbs) agent skill teaches coding assistants such as Claude Code, Codex, Cursor, and Copilot how to build a breadcrumb trail from the project map: resolving the current node, walking its ancestors, and titling and linking each crumb. To install it, see [Uniform agent skills](https://docs.uniform.app/docs/guides/ai/agent-skills).

### Node properties

Each `ProjectMapNode` contains:

| Property | Type | Description |
| --- | --- | --- |
| `id` | `string` | Unique node identifier |
| `path` | `string` | URL path (e.g., `/about`, `/blog/:slug`) |
| `name` | `string` | Display name |
| `type` | `string` | Node type: `composition`, `placeholder`, `redirect`, or `alias` |
| `compositionId` | `string` | Linked composition ID (when type is `composition`) |
| `compositionData` | `object` | Composition metadata including editions and locales |
| `locales` | `object` | Locale-specific path overrides |
| `data` | `object` | Custom node data |

### Query options

`getNodes` accepts these parameters:

| Option | Type | Description |
| --- | --- | --- |
| `projectMapId` | `string` | Required. The project map definition ID. |
| `path` | `string` | Filter nodes starting at this path. |
| `depth` | `number` | How many levels deep to traverse. |
| `includeDescendants` | `boolean` | Include all descendant nodes. |
| `compositionId` | `string` | Find the node for a specific composition. |

---

## Getting subtrees

`getSubtree` returns nodes in a tree structure with parent/child/sibling references. This is ideal for building navigation menus:

```ts
const tree = await projectMapClient.getSubtree({
  projectMapId: "your-project-map-id",
  path: "/",
});

if (tree) {
  console.log("Root:", tree.name, tree.path);
  for (const child of tree.children ?? []) {
    console.log("  Child:", child.name, child.path);
    for (const grandchild of child.children ?? []) {
      console.log("    Grandchild:", grandchild.name, grandchild.path);
    }
  }
}
```

### ProjectMapSubtree

The subtree node extends `ProjectMapNode` with navigation references:

| Property | Type | Description |
| --- | --- | --- |
| `children` | `ProjectMapSubtree[]` | Child nodes |
| `parent` | `ProjectMapSubtree` | Parent node (references stripped to prevent circular refs) |
| `previousSibling` | `ProjectMapSubtree` | Previous sibling node |
| `nextSibling` | `ProjectMapSubtree` | Next sibling node |

---

## Building navigation

A practical example of building a site navigation from the project map:

```tsx
import { getProjectMapClient } from "@uniformdev/next-app-router";

type NavItem = {
  name: string;
  path: string;
  children: NavItem[];
};

async function getNavigation(): Promise<NavItem[]> {
  const projectMapClient = getProjectMapClient({
    cache: { type: "force-cache" },
  });

  const tree = await projectMapClient.getSubtree({
    projectMapId: process.env.UNIFORM_PROJECT_MAP_ID!,
    path: "/",
    depth: 2,
  });

  if (!tree?.children) return [];

  return tree.children
    .filter((node) => node.type === "composition")
    .map((node) => ({
      name: node.name,
      path: node.path,
      children: (node.children ?? [])
        .filter((child) => child.type === "composition")
        .map((child) => ({
          name: child.name,
          path: child.path,
          children: [],
        })),
    }));
}
```

> **Use an AI coding assistant:**
>
> The [`uniform-navigation`](https://github.com/uniformdev/agent-skills/tree/main/skills/uniform-navigation) agent skill teaches coding assistants such as Claude Code, Codex, Cursor, and Copilot how to model and build header navigation that editors can author and reorder: dropdowns, mega menus, and mobile drawers. To install it, see [Uniform agent skills](https://docs.uniform.app/docs/guides/ai/agent-skills).

---

## Locale path resolution

Use `getNodeLocalePath` to resolve the correct URL path for a given locale. If a node has locale-specific path overrides, this utility returns the override; otherwise it falls back to the default path:

```ts
import { getNodeLocalePath } from "@uniformdev/project-map";

// node.path = "/about"
// node.locales = { fr: { path: "/a-propos" }, de: { path: "/ueber-uns" } }

const frenchPath = getNodeLocalePath(node, "fr");
// Returns: "/a-propos"

const defaultPath = getNodeLocalePath(node, undefined);
// Returns: "/about"
```

---

## Alternate locale URLs

Use `getRouteAlternateLocalesUrls` to generate all locale-specific URLs for a composition route. This is useful for `hreflang` tags and language switchers:

```ts
import { getRouteAlternateLocalesUrls } from "@uniformdev/project-map";

const alternateUrls = getRouteAlternateLocalesUrls(routeResponse);
// Returns: { en: "/about", fr: "/a-propos", de: "/ueber-uns" }
```

Usage in a `<head>` element:

```tsx
{alternateUrls && Object.entries(alternateUrls).map(([locale, url]) => (
  <link key={locale} rel="alternate" hrefLang={locale} href={url} />
))}
```

---

## Active edition resolution

When using content editions (locale-specific composition variants), use `getNodeActiveCompositionEdition` to resolve the correct edition for a given locale:

```ts
import { getNodeActiveCompositionEdition } from "@uniformdev/project-map";

const activeEdition = getNodeActiveCompositionEdition({
  node,
  targetLocale: "fr",
  fallbackWhenLocaleNotMatched: true,
});
```

| Option | Type | Description |
| --- | --- | --- |
| `node` | `ProjectMapNode` | The node to resolve the edition for |
| `targetLocale` | `string \| undefined` | Locale to match. When `undefined`, returns the default. |
| `fallbackWhenLocaleNotMatched` | `boolean` | When `true`, falls back to default if no edition matches the locale. Default: `false`. |

---

## Route matching utility

The `Route` class provides route pattern matching with dynamic segments (`:paramName`):

```ts
import { Route } from "@uniformdev/project-map";

const route = new Route("/blog/:slug");

// Test if a path matches
const result = route.matches("/blog/hello-world");
if (result.match) {
  console.log(result.pathParams); // { slug: "hello-world" }
}

// Expand a route with values
const expanded = route.expand({
  dynamicInputValues: { slug: "my-post" },
});
// Returns: "/blog/my-post"
```

---

## Generating a sitemap

Combine `getNodes` with route resolution to generate a sitemap:

```ts
import { ProjectMapClient } from "@uniformdev/project-map";

async function generateSitemap(baseUrl: string) {
  const client = new ProjectMapClient({
    apiKey: process.env.UNIFORM_API_KEY!,
    projectId: process.env.UNIFORM_PROJECT_ID!,
  });

  const { nodes } = await client.getNodes({
    projectMapId: process.env.UNIFORM_PROJECT_MAP_ID!,
  });

  const urls = (nodes ?? [])
    .filter((node) => node.compositionId && !node.path.includes(":"))
    .map((node) => ({
      url: `${baseUrl}${node.path}`,
      lastModified: new Date().toISOString(),
    }));

  return urls;
}
```

---

## UncachedProjectMapClient

For scenarios that require bypassing the API cache (e.g., admin scripts, preview modes), use `UncachedProjectMapClient`:

```ts
import { UncachedProjectMapClient } from "@uniformdev/project-map";

const client = new UncachedProjectMapClient({
  apiKey: process.env.UNIFORM_API_KEY!,
  projectId: process.env.UNIFORM_PROJECT_ID!,
});
```

---

## Import reference

| Export | Package | Description |
| --- | --- | --- |
| `ProjectMapClient` | `@uniformdev/project-map` | Main project map client |
| `UncachedProjectMapClient` | `@uniformdev/project-map` | Cache-bypassing client |
| `getNodeLocalePath` | `@uniformdev/project-map` | Resolve locale-specific path for a node |
| `getRouteAlternateLocalesUrls` | `@uniformdev/project-map` | Generate all locale URLs for a route |
| `getNodeActiveCompositionEdition` | `@uniformdev/project-map` | Resolve active edition for a locale |
| `Route` | `@uniformdev/project-map` | Route pattern matching and expansion |
| `getProjectMapClient` | `@uniformdev/next-app-router` | Pre-configured client (server-only, App Router) |
