uidudevelopers
Domains

Stories

Fetch blog posts and long-form editorial content from the uidu Stories API.

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.

FieldWhat it holds
namethe title
bodyTiptap JSON — render it with <RichText>. Only getStory returns it
cover, coverDatathe cover image URL, and its metadata as JSON
publishedtrue once publishedAt is set and in the past
publishedAtISO timestamp; a future one means the story is scheduled
publicPath, publicUrlthe story's canonical permalink on uidu (publicUrl on getStory only)
  • listStories returns summaries (id, name, cover, coverData, published, publishedAt, publicPath) without body, so it's cheap. It's a flat array unwrapped from the Relay connection, one page at a time (first / after; paginate with ListStoriesDocument for all of them), newest-created first — drafts included: filter on published yourself.
  • getStory({ id }) takes the story's GlobalID or its slug (generated from the title), and returns the full Story with body, or null. An unknown slug can surface as a thrown UiduError rather than null, so a detail page should handle both.
  • createStory and updateStory need the Bearer client. createStory returns { errors, story } with story = { id, name }; updateStory takes { input: { id, attributes } } and returns the record at story. Validation failures come back in errors, not as a throw. The attributes include name, body (Tiptap JSON), abstract, cover, publishedAt, slug, seoTitle, seoMetaDescription and storyTagList — the full list is StoryAttributes in 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 publishedAt on your side: the API orders by creation date.
  • A GlobalID contains / and :, hence encodeURIComponent in the link and decodeURIComponent in the page. Since getStory also 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 publishedAt is a draft; setting it publishes it (a future date schedules it). You can also pass publishedAt straight to createStory.
  • 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.

FunctionKindAuthCLIDescription
createStorywriteBearerstories createCreate a story; returns the payload as-is, { errors, story }, validation failures included.
getStoryreadpublic tokenstories getOne story with its rich-text body, or null.
listStoriesreadpublic tokenstories listThe workspace's stories as a flat array of summaries (no body); page with first/after.
updateStorywriteBearerstories updateUpdate a story: { input: { id, attributes } } (create it with createStory). Returns the payload (with errors) or null.

Types: Story

CLI:

Entitylistgetcreateupdatedelete
stories✓✓✓✓—

On this page