# Stories

URL: https://developers.uidu.org/docs/domains/stories

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

* **Use it for:** [Public website](https://developers.uidu.org/docs/paths/public-website.md) · [Server integration](https://developers.uidu.org/docs/paths/server-integration.md)
* **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](https://developers.uidu.org/docs/foundations/authentication.md)

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>`](https://developers.uidu.org/docs/tools/react/components.md). 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)                              |

* `listStories` returns summaries (`id`, `name`, `cover`, `coverData`, `published`, `publishedAt`,
  `publicPath`) &#x2A;*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](https://developers.uidu.org/docs/reference/schema.md).

## Use cases

### Blog index and post page

Goal: a `/stories` listing of what's published, newest first, and a page for each story.

```tsx
// 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.

```ts
// 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](https://developers.uidu.org/docs/paths/server-integration.md); the write contract:
  [Writes](https://developers.uidu.org/docs/foundations/writes.md).

## API at a glance

Every `@uidu/client` function in **Stories**, generated from the source — the [full reference](https://developers.uidu.org/docs/reference/client.md#stories) has the rest of the SDK.

| Function                                                                                       | Kind  | Auth         | CLI              | Description                                                                                                                  |
| ---------------------------------------------------------------------------------------------- | ----- | ------------ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| [`createStory`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/stories.ts)   | write | Bearer       | `stories create` | Create a story; returns the payload as-is, `{ errors, story }`, validation failures included.                                |
| [`getStory`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/stories.ts)      | read  | public token | `stories get`    | One story with its rich-text `body`, or null.                                                                                |
| [`listStories`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/stories.ts)   | read  | public token | `stories list`   | The workspace's stories as a flat array of summaries (no `body`); page with `first`/`after`.                                 |
| [`updateStory`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts) | 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` |   ✓  |  ✓  |    ✓   |    ✓   |    —   |
