uidudevelopers
Recipes

Demo app

A complete Next.js 16 example exercising every domain in @uidu/client.

The repo ships with a complete Next.js 16 demo at apps/demo/.

Try it live: api-js-demo.vercel.app — it reads from the seeded demo workspace on staging (demo.uidu.dev).

What it covers

RouteWhat it shows
/CMS landing page rendered block by block with <PageBlocks>
/events, /events/[id]Event listing and detail, with RSVP
/stories, /stories/[id]Stories (blog) listing and detail
/careers, /careers/[id]Job openings and the application flow
/forms, /forms/[id]Any uidu form rendered with <DynamicForm>
/help, /help/[articleId]Help center channel, collections and articles
/donate, /donate/[id]Donation campaigns
/bookings, /coursesCalendars and bookings, courses and lectures
/store, /membershipsStorefront and membership UI patterns
/custom-appA custom app built on the same client
/admin/*Employees, schedule, timesheet, CRM, campaigns, tasks, sites

Where a workspace has no data for a domain, a few pages fall back to sample data so the UI is still visible.

The live demo runs without an API key, so it is read-only: form submissions, RSVPs and the /admin pages need an API key, which the public deploy does not have. Run it locally with UIDU_API_KEY set to try them.

Run it locally

# from the repo root
cp apps/demo/.env.example apps/demo/.env.local
# defaults point at the staging `demo` workspace
yarn install
yarn workspace demo dev

Open http://localhost:3002.

Configuration

Everything is driven by env vars (see apps/demo/.env.example):

VariableDefaultPurpose
UIDU_HOSTuidu.devWhich uidu to talk to. Staging by default; set uidu.org to opt into production
UIDU_WORKSPACEdemoWorkspace subdomain
UIDU_PUBLIC_TOKEN—Optional public read token
UIDU_API_KEY—Server-only Bearer token for mutations and /admin
UIDU_LANDING_SLUGlandingCMS page rendered on /
UIDU_PROJECT_ID, UIDU_CHANNEL_IDresolvedPin a specific CMS project / help-center channel

The CMS project and the help-center channel are resolved from the workspace at runtime, so nothing workspace-specific is committed.

How it's built

lib/uidu.ts — one client

import { createClient } from '@uidu/client';

const host = process.env.UIDU_HOST ?? 'uidu.dev';
const workspace = process.env.UIDU_WORKSPACE ?? 'demo';

export const uidu = createClient({
  workspace,
  endpoint: `https://${workspace}.${host}/graphql`,
  publicToken: process.env.UIDU_PUBLIC_TOKEN,
  apiKey: process.env.UIDU_API_KEY,
});

One client, used from every Server Component and Server Action. No provider needed on the server.

Landing page — CMS to JSX

// app/page.tsx
import { getPage } from '@uidu/client';
import { PageBlocks } from '@uidu/react';
import { ContactForm } from '@/components/blocks/ContactForm';
import { Feature } from '@/components/blocks/Feature';
import { Hero } from '@/components/blocks/Hero';
import { config, defaultProjectId, uidu } from '@/lib/uidu';

export default async function HomePage() {
  const projectId = await defaultProjectId();
  const page = projectId
    ? await getPage(uidu, { projectId, slug: config.landingSlug })
    : null;
  if (!page) return <NotFound />;

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

<PageBlocks> walks the blocks, matches each block's shortname against the components map, and renders it. Block components read their fields with useFields(block.fieldValues) and toText().

Contact form — Server Action mutation

The ModuloContatto block carries a linked Form record. The block hands it to a 'use client' component that submits through a Server Action:

// lib/actions.ts
'use server';
import { createFormResponse } from '@uidu/client';
import { uidu } from './uidu';

export async function submitContactForm(formId, fieldValues, contact) {
  const result = await createFormResponse(uidu, {
    input: {
      attributes: { formId, fieldValuesAttributes: /* … */, contactAttributes: /* … */ },
      event: 'complete!',
    },
  });
  return result?.errors?.length
    ? { ok: false, errors: result.errors }
    : { ok: true, responseId: result.formResponse.id };
}

Help center — one query, full tree

// app/help/page.tsx
const channelId = await defaultChannelId();
const channel = channelId ? await getChannel(uidu, { id: channelId }) : null;

One getChannel call returns the channel with its collections and articles; the page renders the tree without further fetches.

Files at a glance

apps/demo/src/
├── app/
│   ├── page.tsx              # Landing — getPage() + <PageBlocks>
│   ├── events/ stories/ careers/ forms/ help/ donate/ …
│   └── admin/                # API-key pages
├── components/
│   ├── Nav.tsx
│   ├── blocks/               # Hero, Feature, ContactForm (+ client part)
│   ├── themes/               # per-section layout variants
│   └── ui/                   # shadcn primitives
└── lib/
    ├── uidu.ts               # createClient() + project/channel resolution
    └── actions.ts            # Server Actions (forms, RSVP, applications, …)

Patterns to take away

  1. One client instance per app. Server-side, no providers. Pass it to every domain function.
  2. CMS blocks → component map. The CMS holds the shape, your code holds the components.
  3. Server Components for fetching, Server Actions for mutating, Client Components for interactivity.
  4. Linked records. When a block embeds a record (a Form inside a page block), the typed response exposes it inline — no second round trip.
  5. Keep secrets server-side. UIDU_API_KEY is only read in Server Components and Server Actions, never shipped to the browser.

On this page