Domains
Stories
Fetch blog posts and long-form editorial content from the uidu Stories API.
- Use it for: Public website · Server integration
- Reads: public token — works in the browser
- Writes: Bearer (
apiKey) — server-side only - CLI:
uidu stories - Auth: what each token can do and where it may run — Authentication
A Story is uidu's blog and editorial primitive: a long-form piece with a title, a cover image, a rich-text body and the usual publish metadata. Use it for a company blog, news posts, or any "post-like" content that isn't a help-center article.
Mental model
Stories hang directly off the workspace — no container to resolve first.
| Field | What it holds |
|---|---|
name | the title |
body | Tiptap JSON — render it with <RichText>. Only getStory returns it |
cover, coverData | the cover image URL, and its metadata as JSON |
published | true once publishedAt is set and in the past |
publishedAt | ISO timestamp; a future one means the story is scheduled |
publicPath, publicUrl | the story's canonical permalink on uidu (publicUrl on getStory only) |
listStoriesreturns summaries (id,name,cover,coverData,published,publishedAt,publicPath) withoutbody, so it's cheap. It's a flat array unwrapped from the Relay connection, one page at a time (first/after;paginatewithListStoriesDocumentfor all of them), newest-created first — drafts included: filter onpublishedyourself.getStory({ id })takes the story's GlobalID or its slug (generated from the title), and returns the fullStorywithbody, ornull. An unknown slug can surface as a thrownUiduErrorrather thannull, so a detail page should handle both.createStoryandupdateStoryneed the Bearer client.createStoryreturns{ errors, story }withstory={ id, name };updateStorytakes{ input: { id, attributes } }and returns the record atstory. Validation failures come back inerrors, not as a throw. The attributes includename,body(Tiptap JSON),abstract,cover,publishedAt,slug,seoTitle,seoMetaDescriptionandstoryTagList— the full list isStoryAttributesin the Schema.
Use cases
Blog index and post page
Goal: a /stories listing of what's published, newest first, and a page for each story.
// src/app/stories/page.tsx
import Link from 'next/link';
import { listStories } from '@uidu/client';
import { uidu } from '@/lib/uidu';
export default async function StoriesPage() {
const stories = await listStories(uidu, { first: 50 });
const published = stories
.filter((s) => s.published !== false)
.sort((a, b) => String(b.publishedAt ?? '').localeCompare(String(a.publishedAt ?? '')));
return (
<ul>
{published.map((s) => (
<li key={s.id}>
<Link href={`/stories/${encodeURIComponent(s.id)}`}>{s.name}</Link>
</li>
))}
</ul>
);
}
// src/app/stories/[id]/page.tsx
import { notFound } from 'next/navigation';
import { getStory } from '@uidu/client';
import { RichText } from '@uidu/react';
import { uidu } from '@/lib/uidu';
export default async function StoryPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const story = await getStory(uidu, { id: decodeURIComponent(id) }).catch(() => null);
if (!story || story.published === false) notFound();
return (
<article>
{story.cover && <img src={story.cover} alt="" />}
<h1>{story.name}</h1>
{story.publishedAt && (
<time dateTime={story.publishedAt as string}>
{new Date(story.publishedAt as string).toLocaleDateString()}
</time>
)}
<RichText doc={story.body} />
</article>
);
}- The listing is sorted by
publishedAton your side: the API orders by creation date. - A GlobalID contains
/and:, henceencodeURIComponentin the link anddecodeURIComponentin the page. SincegetStoryalso accepts a slug, you can route on the slug instead if you keep one per story. .catch(() => null)folds "not found" and a failed request into a 404 — fine for a public page; log the error if you need to tell them apart.
Publish a story from your server
Goal: an internal tool (or an import job) creates a story and publishes it.
// src/app/admin/stories/actions.ts
'use server';
import { createStory, updateStory } from '@uidu/client';
import { uiduAdmin } from '@/lib/uidu-admin'; // the apiKey client — server-only
export async function publishStory(title: string, body: unknown) {
const created = await createStory(uiduAdmin, {
input: { attributes: { name: title, body } },
});
if (!created || created.errors?.length) {
return { ok: false, errors: created?.errors?.map((e) => e.name) ?? [] };
}
const id = created.story!.id;
const published = await updateStory(uiduAdmin, {
input: { id, attributes: { publishedAt: new Date().toISOString() } },
});
if (published?.errors?.length) {
return { ok: false, errors: published.errors.map((e) => e.name) };
}
return { ok: true, id };
}- A story without
publishedAtis a draft; setting it publishes it (a future date schedules it). You can also passpublishedAtstraight tocreateStory. - Never call these from the browser: the Bearer token would ship with it. The same from the
terminal:
uidu stories create --attributes '{"name":"Our 2026 roadmap"}' --json. The client set-up: Server integration; the write contract: Writes.
API at a glance
Every @uidu/client function in Stories, generated from the source — the full reference has the rest of the SDK.
| Function | Kind | Auth | CLI | Description |
|---|---|---|---|---|
createStory | write | Bearer | stories create | Create a story; returns the payload as-is, { errors, story }, validation failures included. |
getStory | read | public token | stories get | One story with its rich-text body, or null. |
listStories | read | public token | stories list | The workspace's stories as a flat array of summaries (no body); page with first/after. |
updateStory | write | Bearer | stories update | Update a story: { input: { id, attributes } } (create it with createStory). Returns the payload (with errors) or null. |
Types: Story
CLI:
| Entity | list | get | create | update | delete |
|---|---|---|---|---|---|
stories | ✓ | ✓ | ✓ | ✓ | — |