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
| Route | What 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, /courses | Calendars and bookings, courses and lectures |
/store, /memberships | Storefront and membership UI patterns |
/custom-app | A 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 devOpen http://localhost:3002.
Configuration
Everything is driven by env vars (see apps/demo/.env.example):
| Variable | Default | Purpose |
|---|---|---|
UIDU_HOST | uidu.dev | Which uidu to talk to. Staging by default; set uidu.org to opt into production |
UIDU_WORKSPACE | demo | Workspace subdomain |
UIDU_PUBLIC_TOKEN | — | Optional public read token |
UIDU_API_KEY | — | Server-only Bearer token for mutations and /admin |
UIDU_LANDING_SLUG | landing | CMS page rendered on / |
UIDU_PROJECT_ID, UIDU_CHANNEL_ID | resolved | Pin 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
- One client instance per app. Server-side, no providers. Pass it to every domain function.
- CMS blocks → component map. The CMS holds the shape, your code holds the components.
- Server Components for fetching, Server Actions for mutating, Client Components for interactivity.
- Linked records. When a block embeds a record (a Form inside a page block), the typed response exposes it inline — no second round trip.
- Keep secrets server-side.
UIDU_API_KEYis only read in Server Components and Server Actions, never shipped to the browser.