# Public website

URL: https://developers.uidu.org/docs/paths/public-website

> Build a public site on uidu, from an empty folder to CMS pages, a contact form and an events section, rendered statically.

Use this path when you're building something **anyone can visit**: a landing page, an
organisation's site, a blog, an events calendar. The pages read public content with the public
token. The one thing a visitor writes, a contact form, goes through a server action that holds the
workspace's Bearer token.

By the end you'll have a Next.js site that renders pages from a uidu **Site**, takes contact-form
submissions, lists events with a page for each one, and pre-renders all of it at build time.

### Scaffold the app

```bash
npm create uidu-app@latest my-site
```

The default `minimal` template gives you Next.js 16 (App Router), Tailwind, a configured
`src/lib/uidu.ts` and an `.env.local` filled in from the prompts. Its home page reads a **legacy CMS
Project** with `<PageBlocks>` and asks for `UIDU_PROJECT_ID` and `UIDU_LANDING_SLUG`. Step 3
replaces that page with one that reads a Site, so you can leave those two empty. Other templates and
every flag: [create-uidu-app](https://developers.uidu.org/docs/tools/create-uidu-app.md).

Already have a Next.js app? Add the packages instead: `npm install @uidu/client @uidu/react swr`
(see [Installation](https://developers.uidu.org/docs/foundations/installation.md)), then write the file in step 2 yourself.

### Create the client

One client for the whole site. It reads with the public token and writes with the API key, so
import it only from server code: Server Components, server actions, `generateStaticParams`.

```ts
// src/lib/uidu.ts
import { createClient } from '@uidu/client';

export const uidu = createClient({
  workspace: process.env.UIDU_WORKSPACE ?? '',
  publicToken: process.env.UIDU_PUBLIC_TOKEN,
  apiKey: process.env.UIDU_API_KEY, // server-only
});
```

Which token does what, where you get each one, and why `apiKey` never reaches the browser:
[Authentication](https://developers.uidu.org/docs/foundations/authentication.md).

### Render a page from your Site

A **Site** holds your pages. Each page has its own field values and an ordered list of **blocks**,
and each block has a kind (a block-kind Model) and its own `fields`, keyed by field shortname. Find
the Site by the domain it's served on, get a page by slug, and render each block with the component
for its kind. You write that component map yourself: `<PageBlocks>` from `@uidu/react` reads legacy
Project pages, not Sites.

```tsx
// src/lib/site.ts
import { cache } from 'react';
import { getSiteByDomain } from '@uidu/client';
import { uidu } from './uidu';

// The domain the Site is served on, as set in uidu.
export const getSite = cache(() => getSiteByDomain(uidu, { domain: 'www.acme.org' }));

// src/components/site-blocks.tsx
import type { ComponentType } from 'react';
import type { SiteBlock } from '@uidu/client';
import { RichText, toText } from '@uidu/react';

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.
const components: Record<string, ComponentType<{ block: SiteBlock }>> = { Hero };

export function SiteBlocks({ blocks }: { blocks: SiteBlock[] }) {
  return blocks.map((block) => {
    const Component = components[block.model.name ?? ''];
    return Component ? <Component key={block.id} block={block} /> : null;
  });
}

// src/app/page.tsx
import { getPageBySlug } from '@uidu/client';
import { notFound } from 'next/navigation';
import { SiteBlocks } from '@/components/site-blocks';
import { getSite } from '@/lib/site';
import { uidu } from '@/lib/uidu';

export default async function HomePage() {
  const site = await getSite();
  if (!site) notFound();

  const page = await getPageBySlug(uidu, { siteId: site.id, slug: 'home' });
  if (!page) notFound();

  return <SiteBlocks blocks={page.blocks} />;
}
```

The block map is keyed on `block.model.name` because the API doesn't expose a block kind's
shortname yet. Pages, singletons such as the header and footer, drafts and previews:
[CMS](https://developers.uidu.org/docs/domains/cms.md).

### Add a contact form

Build the form in uidu, then get it with `getForm` and render it with `<DynamicForm>`, which picks an
input for each question and hands the answers to your `action`. The action is a **server action**:
like every write, `createFormResponse` needs the client with the API key. Every answer's `content`
is `{ value }`.

```tsx
// src/app/contact/page.tsx
import { createFormResponse, getForm } from '@uidu/client';
import { DynamicForm, type DynamicFormResult, type DynamicFormValues } from '@uidu/react';
import { notFound } from 'next/navigation';
import { uidu } from '@/lib/uidu';

const FORM_ID = 'Z2lkOi8v…'; // from the Forms app, or `uidu forms list`

export default async function ContactPage() {
  const form = await getForm(uidu, { id: FORM_ID });
  if (!form) notFound();

  async function submit(values: DynamicFormValues): Promise<DynamicFormResult> {
    'use server';
    const result = await createFormResponse(uidu, {
      input: {
        attributes: {
          formId: FORM_ID,
          fieldValuesAttributes: values.fieldValues.map((fv) => ({
            fieldId: fv.fieldId,
            content: { value: fv.value },
          })),
          contactAttributes: values.contact && {
            email: values.contact.email,
            contactableAttributes: {
              kind: 'person',
              firstName: values.contact.firstName,
              lastName: values.contact.lastName || undefined,
            },
          },
        },
        event: 'complete!',
      },
    });

    if (result?.errors?.length) return { ok: false, errors: result.errors };
    return { ok: true, meta: { responseId: result?.formResponse?.id } };
  }

  return <DynamicForm form={form} action={submit} submitLabel="Send" />;
}
```

Validation failures come back in `errors`, not as a throw; `<DynamicForm>` shows them above the
button. The input shape and drafts: [Forms](https://developers.uidu.org/docs/domains/forms.md). The component's props:
[React components](https://developers.uidu.org/docs/tools/react/components.md). Errors on every write:
[Writes](https://developers.uidu.org/docs/foundations/writes.md).

### List events, with a page for each

A listing with `listEvents` and a detail page with `getEvent`. Put the detail route under a scope
path, `/event/[id]`, not at the root: a root-level `/[id]` also catches requests like
`/favicon.ico` and turns them into failed queries.

```tsx
// src/app/events/page.tsx
import { listEvents } from '@uidu/client';
import Link from 'next/link';
import { uidu } from '@/lib/uidu';

export default async function EventsPage() {
  const events = await listEvents(uidu, { first: 50 });

  return (
    <ul>
      {events.map((event) => (
        <li key={event.id}>
          <Link href={`/event/${encodeURIComponent(event.id)}`}>{event.name}</Link>
        </li>
      ))}
    </ul>
  );
}

// src/app/event/[id]/page.tsx
import { getEvent } from '@uidu/client';
import { RichText } from '@uidu/react';
import { notFound } from 'next/navigation';
import { uidu } from '@/lib/uidu';

export default async function EventPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const event = await getEvent(uidu, { id: decodeURIComponent(id) });
  if (!event) notFound();

  return (
    <article>
      <h1>{event.name}</h1>
      {event.instance?.beginsAt && (
        <time>{new Date(event.instance.beginsAt as string).toLocaleString()}</time>
      )}
      <RichText doc={event.body} />
    </article>
  );
}
```

Instances, addresses, calendar links and RSVPs with `createAttendance`: [Events](https://developers.uidu.org/docs/domains/events.md).

### Render every page at build time

`generateStaticParams` tells Next.js which pages to pre-render. `listSitePages` already returns all
of a Site's published pages; events come one page at a time, so walk them all with `paginate` and
the generated `ListEventsDocument`.

```tsx
// src/app/[slug]/page.tsx — every Site page except the home page
import { getPageBySlug, listSitePages } from '@uidu/client';
import { notFound } from 'next/navigation';
import { SiteBlocks } from '@/components/site-blocks';
import { getSite } from '@/lib/site';
import { uidu } from '@/lib/uidu';

export const dynamicParams = false; // only the slugs below; anything else is a 404

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

export default async function SitePage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const site = await getSite();
  const page = site && (await getPageBySlug(uidu, { siteId: site.id, slug }));
  if (!page) notFound();
  return <SiteBlocks blocks={page.blocks} />;
}

// src/app/event/[id]/page.tsx — add next to the page component from step 5
import { paginate, ListEventsDocument } from '@uidu/client';

export async function generateStaticParams() {
  const events = await paginate(uidu, ListEventsDocument, {
    variables: { first: 100 },
    extract: (data) => data.currentWorkspace?.events,
  });
  return events.map((event) => ({ id: event.id }));
}
```

`/` stays with `app/page.tsx` from step 3. With `dynamicParams = false` the root `[slug]` route only answers the slugs it was built with, so it
doesn't catch stray requests. Events keep the default: one published after the build still renders
on its first visit. How `paginate` walks a connection: [Pagination & field
values](https://developers.uidu.org/docs/foundations/pagination-and-field-values.md). A sitemap and revalidation on top of the same
route: [Dynamic CMS routes](https://developers.uidu.org/docs/recipes/dynamic-cms-routes.md).

### Deploy

Set the same three variables wherever the site builds and runs:

```bash
UIDU_WORKSPACE=acme
UIDU_PUBLIC_TOKEN=...   # public reads
UIDU_API_KEY=...        # the contact form's server action; server-only
```

They're needed **at build time** too, because `generateStaticParams` and the static pages query
uidu during the build. Keep `UIDU_API_KEY` a plain server variable: never prefix it `NEXT_PUBLIC_`.

## Where to go next

* **More public content** — [Stories](https://developers.uidu.org/docs/domains/stories.md) for a blog, [Donations](https://developers.uidu.org/docs/domains/donations.md)
  for campaigns and a donate form, [Help center](https://developers.uidu.org/docs/domains/help-center.md) for an FAQ,
  [Jobs](https://developers.uidu.org/docs/domains/jobs.md) for a careers page.
* **Recipes** — [Multi-step forms](https://developers.uidu.org/docs/recipes/multi-step-forms.md), [File upload](https://developers.uidu.org/docs/recipes/file-upload.md),
  [Search as you type](https://developers.uidu.org/docs/recipes/search-as-you-type.md), and the [demo app](https://developers.uidu.org/docs/recipes/demo-app.md),
  a full Next.js site built this way.
* **Reference** — every function in the [client reference](https://developers.uidu.org/docs/reference/client.md), every component and
  hook in the [React reference](https://developers.uidu.org/docs/tools/react/components.md).
