# CMS

URL: https://developers.uidu.org/docs/domains/cms

> Read Sites (the new CMS) and legacy CMS Projects with typed GraphQL functions.

* **Use it for:** [Public website](https://developers.uidu.org/docs/paths/public-website.md) · [Server integration](https://developers.uidu.org/docs/paths/server-integration.md)
* **Reads:** public token — works in the browser
* **Writes:** Bearer (`apiKey`) — server-side only
* **CLI:** `uidu projects`, `uidu sites`
* **Auth:** what each token can do and where it may run — [Authentication](https://developers.uidu.org/docs/foundations/authentication.md)

The CMS is where a workspace keeps its website: pages, the blocks placed on them, and the header
and footer they share. uidu has two CMS generations and `@uidu/client` reads both; new sites go on
**Sites**.

## Mental model

|                | **Sites** (new CMS)                                                                 | **Projects** (legacy CMS)             |
| -------------- | ----------------------------------------------------------------------------------- | ------------------------------------- |
| Container      | `Site`                                                                              | `Project`                             |
| Page type      | a page-kind Model                                                                   | `Template`                            |
| Page           | a ModelItem of a page type                                                          | `Page`                                |
| Block kind     | a block-kind Model                                                                  | `Block`                               |
| Block instance | a block ModelItem placed on a page                                                  | `PageBlock`                           |
| Functions      | `getSiteByDomain`, `getSite`, `getPageBySlug`, `listSitePages`, `getSingletonBlock` | `getPage`, `listPages`, `getTemplate` |

**New sites: use Sites.** Projects keep working unchanged for sites already deployed on them, and a
Project can be migrated to a Site from the CMS app. How Sites sit on top of Models:
[Data model](https://developers.uidu.org/docs/foundations/data-model.md).

**Sites.** A `Site` is `{ id, name, shortname, domain, preferences }` — `preferences` holds its theme
settings as JSON. You find it by the domain it is served on (`getSiteByDomain`) or by id
(`getSite`, for previews, where there is no domain to go by). Under it:

```
Site
├── page                  SitePage: { id, name, slug, publishedAt, status, model, fields, blocks }
│   └── blocks[]          SiteBlock: { id, name, model, fields, position, placementId }
└── singleton             header, footer, nav — same SitePage shape, fetched by shortname
```

* `fields` is the item's field values **keyed by field shortname** (`page.fields.title`), both on
  a page and on each block.
* `model` is the page type or block kind: `{ id, name, kind }`, `kind` being `page`, `block` or
  `singleton`.
* `blocks` come back &#x2A;*sorted by `position`**. `placementId` is the placement itself (the child
  FieldValue): what reordering or removing a block targets.
* `status` is `Draft`, `Published` or `Scheduled`. `getPageBySlug` returns published pages only,
  and `listSitePages` published pages only, unless you pass `includeDrafts: true`.
* `listSitePages` returns `SitePageSummary[]` (the page without `fields` and `blocks`), leaves
  singletons out, and keeps one page type with `pageType` — matched against the Model's id **or**
  its name. It reads every page of the Site and filters on your side of the call; pages come back
  in creation order, so sort them yourself.
* `getSingletonBlock` finds a singleton by its **Model's shortname** (`header`, `footer`) and
  returns the same `SitePage` shape, or null.

<Callout type="warn">
  Block kinds have a stable `shortname`, but the uidu API does not expose it on
  `Model` yet — key your component map on `block.model.name` until it does.
  `<PageBlocks>` from `@uidu/react` renders legacy Project pages only; for a
  Site you write the map yourself (below).
</Callout>

**Projects (legacy).** `Project → Template → Page → PageBlock`, each `PageBlock` with
`fieldValues[]`. `getPage` returns the page with:

* `metadata` — `metaTitle`, `metaDescription`, `metaKeywords`, `metaImage` (each `string | null`);
* `pageBlocks[]` — each with its own `shortname` and `templateBlock.block.shortname`, the block
  "type" that `<PageBlocks>` keys on;
* `fieldValues[]` — `field.shortname` is the key, `content` the value (JSON), and `linkedRecord`
  the record a field points at. When that record is a Form, its questions come along (as
  `linkedRecord.inputs`), ready for `<DynamicForm>`.

`listPages` returns `{ id, name, slug }` only — cheap, but **one Relay page** at a time;
`getTemplate` returns a template with every page that uses it and their blocks (without page
`metadata` or linked forms). `Page` and `Template` are inferred from the live schema, so they
update the next time codegen runs.

**Both generations.** Every function on this page is server-safe and has no React or framework
dependency — Node, Bun, Deno, the browser, edge runtimes. Every read works with a `publicToken`
client. A `get*` returns `null` when the page doesn't exist or isn't published; a failed request
**throws** a `UiduError` (`GRAPHQL_ERROR` for schema or permission errors, `NETWORK_ERROR` for
connectivity) — see [Errors](https://developers.uidu.org/docs/foundations/errors.md). Writes (`createSite`, and `createProject`,
`createPage`, `createPageBlock`, `createFieldValue` on Projects) need the Bearer client and return
their payload with `errors` instead of throwing: [Writes](https://developers.uidu.org/docs/foundations/writes.md).

## Use cases

### Every page of a Site, with its header

Goal: one route renders any page of the Site by slug, pre-rendered at build time, with the header
singleton on top.

```tsx
// src/lib/site.ts — one request per render, shared by layout and page
import { cache } from 'react';
import { getSiteByDomain } from '@uidu/client';
import { uidu } from './uidu';

export const getSite = cache(() => getSiteByDomain(uidu, { domain: 'www.acme.org' }));

// src/app/[slug]/page.tsx
import type { ComponentType } from 'react';
import { notFound } from 'next/navigation';
import {
  getPageBySlug,
  getSingletonBlock,
  listSitePages,
  type SiteBlock,
} from '@uidu/client';
import { RichText, toText } from '@uidu/react';
import { getSite } from '@/lib/site';
import { uidu } from '@/lib/uidu';

function Hero({ block }: { block: SiteBlock }) {
  return (
    <section>
      <h1>{toText(block.fields.title)}</h1>
      <RichText doc={block.fields.body} />
    </section>
  );
}

// Keyed by the block kind's name in uidu (block.model.name).
const components: Record<string, ComponentType<{ block: SiteBlock }>> = { Hero };

export async function generateStaticParams() {
  const site = await getSite();
  if (!site) return [];
  const pages = await listSitePages(uidu, { siteId: site.id });
  return pages.filter((p) => p.slug).map((p) => ({ slug: p.slug! }));
}

export default async function SitePageRoute({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const site = await getSite();
  if (!site) notFound();

  const [page, header] = await Promise.all([
    getPageBySlug(uidu, { siteId: site.id, slug }),
    getSingletonBlock(uidu, { siteId: site.id, shortname: 'header' }),
  ]);
  if (!page) notFound();

  return (
    <>
      {header && <header>{toText(header.fields.title)}</header>}
      <main>
        {page.blocks.map((block) => {
          const Component = components[block.model.name ?? ''];
          return Component ? <Component key={block.id} block={block} /> : null;
        })}
      </main>
    </>
  );
}
```

* A block whose kind isn't in the map renders nothing — add a fallback while you build the map out.
* `getSiteByDomain` returns `null` for a domain no Site is served on. `cache()` makes the layout and
  the page share one request.
* The same steps from an empty folder, with a contact form and events:
  [Public website](https://developers.uidu.org/docs/paths/public-website.md).

### A blog listing, a sitemap and draft previews

Goal: list the posts of one page type, emit a sitemap, and let editors preview unpublished pages.

```tsx
// src/app/blog/page.tsx
import Link from 'next/link';
import { listSitePages } from '@uidu/client';
import { uidu } from '@/lib/uidu';
import { getSite } from '@/lib/site';

export default async function BlogPage() {
  const site = await getSite();
  if (!site) return null;

  const posts = await listSitePages(uidu, { siteId: site.id, pageType: 'Blog post' });
  const newest = [...posts].sort((a, b) =>
    (b.publishedAt ?? '').localeCompare(a.publishedAt ?? ''),
  );

  return (
    <ul>
      {newest.map((p) => (
        <li key={p.id}>
          <Link href={`/${p.slug}`}>{p.name}</Link>
        </li>
      ))}
    </ul>
  );
}

// src/app/sitemap.ts
import type { MetadataRoute } from 'next';
import { listSitePages } from '@uidu/client';
import { uidu } from '@/lib/uidu';
import { getSite } from '@/lib/site';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const site = await getSite();
  if (!site?.domain) return [];
  const pages = await listSitePages(uidu, { siteId: site.id });
  return pages
    .filter((p) => p.slug)
    .map((p) => ({
      url: `https://${site.domain}/${p.slug}`,
      lastModified: p.publishedAt ?? undefined,
    }));
}

// In the page route: drafts only while Next's draft mode is on.
import { draftMode } from 'next/headers';

const { isEnabled } = await draftMode();
const page = await getPageBySlug(uidu, { siteId: site.id, slug, includeDrafts: isEnabled });
```

* `pageType: 'Blog post'` matches the page type's **name**; its Model id works too and survives a
  rename.
* `includeDrafts` is not gated by the token — anyone who can read the Site can pass it. Turn it on
  only behind your own preview check, as above.
* In a preview there may be no domain yet: resolve the Site with `getSite(uidu, { id })` instead.

### A site still on a legacy Project

Goal: render every page of a Project with `<PageBlocks>`, statically, including a contact form a
block links to.

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

const projectId = process.env.UIDU_PROJECT_ID!;

export async function generateStaticParams() {
  // listPages(uidu, { projectId }) is one page of results; walk them all.
  const pages = await paginate(uidu, ListPagesDocument, {
    variables: { projectId, first: 100 },
    extract: (data) => data.currentWorkspace?.project?.pages,
  });
  return pages
    .filter((p) => p.slug && p.slug !== 'home')
    .map((p) => ({ slug: p.slug! }));
}

export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const page = await getPage(uidu, { projectId, slug });
  return {
    title: page?.metadata?.metaTitle ?? page?.name ?? undefined,
    description: page?.metadata?.metaDescription ?? undefined,
  };
}

export default async function CmsPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const page = await getPage(uidu, { projectId, slug });
  if (!page) notFound(); // doesn't exist, or unpublished

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

// Inside ContactForm ({ block }): the form a field links to.
const formField = block.fieldValues?.find(
  (fv) => fv.linkedRecord && 'inputs' in fv.linkedRecord,
);
```

* `<PageBlocks>` keys each block on `templateBlock.block.shortname` (falling back to the block's own
  `shortname`); inside a block, `useFields(block.fieldValues)` gives `{ shortname: value }`. Both:
  [React components](https://developers.uidu.org/docs/tools/react/components.md).
* The project id comes from `uidu projects list` or `listProjects`. More on this route, ISR and a
  sitemap: [Dynamic CMS routes](https://developers.uidu.org/docs/recipes/dynamic-cms-routes.md).
* `getTemplate(uidu, { projectId, templateId })` returns the template with all its pages and their
  blocks in one request — handy to render a section of pages that share a layout.

## API at a glance

Every `@uidu/client` function in **CMS — Sites**, generated from the source — the [full reference](https://developers.uidu.org/docs/reference/client.md#sites) has the rest of the SDK.

| Function                                                                                         | Kind  | Auth         | CLI            | Description                                                                                                                                                                                             |
| ------------------------------------------------------------------------------------------------ | ----- | ------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`createSite`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/sites.ts)        | write | Bearer       | `sites create` | Provisions a new Site in the workspace/account (`tenantId`). Bearer token (`apiKey`) — this is workspace admin territory, never a public write. Returns the mutation payload as-is, including `errors`. |
| [`getPageBySlug`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/sites.ts)     | read  | public token | —              | A page of the Site by slug, with its field values and its ordered blocks. Only published pages unless `includeDrafts`.                                                                                  |
| [`getSingletonBlock`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/sites.ts) | read  | public token | —              | A singleton of the Site — the header, the footer, the nav — by its Model's shortname, with its field values and blocks.                                                                                 |
| [`getSite`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/sites.ts)           | read  | public token | —              | A Site by its id — for previews, where there is no domain to go by.                                                                                                                                     |
| [`getSiteByDomain`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/sites.ts)   | read  | public token | —              | The Site served on `domain` (e.g. `www.acme.org`), or null.                                                                                                                                             |
| [`listSitePages`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/sites.ts)     | read  | public token | —              | The Site's pages, for navigation and sitemaps: published only unless `includeDrafts`, singletons (header, footer) left out.                                                                             |

**Types:** `Site`, `SiteBlock`, `SiteFields`, `SiteModel`, `SitePage`, `SitePageSummary`

**CLI:**

| Entity  | list | get | create | update | delete |
| ------- | :--: | :-: | :----: | :----: | :----: |
| `sites` |   —  |  —  |    ✓   |    —   |    —   |

## API at a glance

Every `@uidu/client` function in &#x2A;*CMS — Projects (legacy)**, generated from the source — the [full reference](https://developers.uidu.org/docs/reference/client.md#cms) has the rest of the SDK.

| Function                                                                                      | Kind  | Auth         | CLI              | Description                                                                                                                                                                   |
| --------------------------------------------------------------------------------------------- | ----- | ------------ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`createFieldValue`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/cms.ts) | write | Bearer       | `field create`   | Create a field value, e.g. set a page block's field (`fieldId`, `content`). Returns the payload (with `errors`) or null; the new value is at `fieldValue.node`.               |
| [`createPage`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/cms.ts)       | write | Bearer       | `page create`    | Create a legacy CMS page from a `templateId`, with `slug` and optional `metadataAttributes` (there is no `projectId` attribute). Returns the payload (with `errors`) or null. |
| [`createPageBlock`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/cms.ts)  | write | Bearer       | `block create`   | Place a template block on a page (`pageId`, `templateBlockId`, both numeric). Returns the payload (with `errors`) or null.                                                    |
| [`createProject`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/cms.ts)    | write | Bearer       | `project create` | Create a legacy CMS project (`name`, `slug`, `domain`). Returns the payload (with `errors`) or null; the new project is at `project.node`.                                    |
| [`getPage`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/cms.ts)          | read  | public token | `page get`       | A legacy CMS page by project and `slug`, with its metadata and ordered `pageBlocks` (and any linked form's questions), or null.                                               |
| [`getTemplate`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/cms.ts)      | read  | public token | —                | A legacy CMS template by id, with every page it holds and their blocks, or null.                                                                                              |
| [`listPages`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/cms.ts)        | read  | public token | `pages list`     | The pages of a legacy CMS project (`id` and `slug`), one Relay page at a time; use `paginate` with `ListPagesDocument` for all of them.                                       |
| [`listProjects`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/cms.ts)     | read  | public token | `projects list`  | The workspace's CMS projects. A page lives in one, and `getPage` needs its id.                                                                                                |

**Types:** `Page`, `Template`

**CLI:**

| Entity     | list | get | create | update | delete |
| ---------- | :--: | :-: | :----: | :----: | :----: |
| `projects` |   ✓  |  —  |    —   |    —   |    —   |
