uidudevelopers
Domains

Help Center

Fetch help center channels, KB collections, and articles.

The help center is a workspace's self-service support site: channels of knowledge-base articles, grouped by topic. getChannel reads a whole channel — its collections and their articles — in one request, which is usually all a help-center front end needs.

Mental model

Workspace
└── Channel                 { id, name, isDefault } — the help center itself
    ├── kbCollections       topics: { id, name, description, iconData }
    │   └── kbArticles      { id, name, body, updatedAt, workspaceTagList }
    └── channelKbCollections   { position, kbCollection { id } } — the order inside this channel
  • Which channel. A workspace can have several. listChannels marks the main one with isDefault: code that has no channel id configured should prefer it over the first row.
  • One request, capped. getChannel fetches the last 100 collections and the last 100 articles of each. Past that, read collections and articles directly with the Knowledge base functions, which paginate.
  • Order. Articles inside a collection come back ordered by their position. A collection's position in this channel lives in channelKbCollections (by kbCollection.id), not on the collection — sort with it when the order matters.
  • Drafts are left out server-side: every article you get here is published.
  • Field shapes. A collection's name and description are JSON — usually a string, possibly a localization map like { en, it }; iconData is icon metadata (JSON). An article's name is a string, its body is rich-text JSON (render it with <RichText>), and workspaceTagList is its tags, { id, name, color }.
  • Search. Knowledge-base articles aren't in the workspace search index, so search won't find them: filter the channel's articles in memory.
  • Writes. createChannel, updateChannel and deleteChannel need the Bearer client and return their payload with errors (the new channel is at channel.node). Collections and articles are written through the Knowledge base functions.

Use cases

Help center home

Goal: a /help page that shows the default channel's topics in the channel's own order, each with its articles.

// src/lib/help.ts
import { cache } from 'react';
import { getChannel, listChannels } from '@uidu/client';
import { uidu } from './uidu';

export const getHelpChannel = cache(async () => {
  const channels = await listChannels(uidu, { first: 25 });
  const main = channels.find((c) => c.isDefault) ?? channels[0];
  return main ? getChannel(uidu, { id: main.id }) : null;
});

// A collection's name/description may be a plain string or a { en, it, … } map.
export function label(value: unknown, locale = 'en'): string {
  if (typeof value === 'string') return value;
  if (value && typeof value === 'object') {
    const map = value as Record<string, unknown>;
    return String(map[locale] ?? Object.values(map)[0] ?? '');
  }
  return '';
}

// src/app/help/page.tsx
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { getHelpChannel, label } from '@/lib/help';

export default async function HelpHomePage() {
  const channel = await getHelpChannel();
  if (!channel) notFound();

  const position = new Map(
    (channel.channelKbCollections?.edges ?? []).flatMap((e) =>
      e?.node ? [[e.node.kbCollection.id, e.node.position] as const] : [],
    ),
  );
  const collections = (channel.kbCollections?.edges ?? [])
    .flatMap((e) => (e?.node ? [e.node] : []))
    .sort((a, b) => (position.get(a.id) ?? 0) - (position.get(b.id) ?? 0));

  return (
    <div>
      <h1>{channel.name}</h1>
      {collections.map((c) => (
        <section key={c.id}>
          <h2>{label(c.name)}</h2>
          <p>{label(c.description)}</p>
          <ul>
            {(c.kbArticles?.edges ?? []).map(
              (a) =>
                a?.node && (
                  <li key={a.node.id}>
                    <Link href={`/help/${encodeURIComponent(a.node.id)}`}>{a.node.name}</Link>
                  </li>
                ),
            )}
          </ul>
        </section>
      ))}
    </div>
  );
}
  • Set the channel id in an env var instead of listChannels if you want to pin one: ids are GlobalIDs, so they belong to one workspace and don't travel between workspaces.
  • cache() lets the home and the article page share one getChannel per render.

Article page

Goal: /help/[articleId] renders one article with its body, under its topic.

// src/app/help/[articleId]/page.tsx
import { notFound } from 'next/navigation';
import { getKbArticle } from '@uidu/client';
import { RichText } from '@uidu/react';
import { label } from '@/lib/help';
import { uidu } from '@/lib/uidu';

export default async function ArticlePage({
  params,
}: {
  params: Promise<{ articleId: string }>;
}) {
  const { articleId } = await params;
  const article = await getKbArticle(uidu, { id: decodeURIComponent(articleId) });
  if (!article) notFound();

  return (
    <article>
      {article.kbCollection && <p>{label(article.kbCollection.name)}</p>}
      <h1>{article.name}</h1>
      <RichText doc={article.body} />
      <small>Updated {new Date(article.updatedAt as string).toLocaleDateString()}</small>
    </article>
  );
}
  • getKbArticle fetches the one article instead of the whole channel. If you already hold the channel (from getHelpChannel()), the article's body is in it too — look it up there and save a request.
  • getKbArticle doesn't check which channel the article belongs to. If you run several channels and must keep them apart, find the article inside the channel instead.

Filter articles on the page

Goal: a quick filter over the help center, by text or by tag, without a server round trip.

// src/app/help/article-filter.tsx
'use client';
import { useState } from 'react';
import Link from 'next/link';

type Item = { id: string; name: string | null; tags: string[] };

export function ArticleFilter({ items }: { items: Item[] }) {
  const [q, setQ] = useState('');
  const needle = q.trim().toLowerCase();
  const hits = needle
    ? items.filter(
        (a) =>
          a.name?.toLowerCase().includes(needle) ||
          a.tags.some((t) => t.toLowerCase().includes(needle)),
      )
    : items;

  return (
    <>
      <input
        type="search"
        value={q}
        onChange={(e) => setQ(e.target.value)}
        aria-label="Filter articles"
      />
      <ul>
        {hits.map((a) => (
          <li key={a.id}>
            <Link href={`/help/${encodeURIComponent(a.id)}`}>{a.name}</Link>
          </li>
        ))}
      </ul>
    </>
  );
}

// In the Server Component, flatten the channel into plain props:
const items = collections.flatMap((c) =>
  (c.kbArticles?.edges ?? []).flatMap((e) => {
    const a = e?.node;
    if (!a) return [];
    const tags = (a.workspaceTagList ?? []).flatMap((t) => (t.name ? [t.name] : []));
    return [{ id: a.id, name: a.name ?? null, tags }];
  }),
);
// <ArticleFilter items={items} />
  • Pass only what the filter needs: the article body is large and doesn't need to reach the browser.

API at a glance

Every @uidu/client function in Help center, generated from the source — the full reference has the rest of the SDK.

FunctionKindAuthCLIDescription
createChannelwriteBearerchannel createCreate a help-center channel. Returns the payload (with errors) or null. The new channel is at channel.node.
deleteChannelwriteBearerchannel deleteDelete a help-center channel by { input: { id } }. Returns the payload (with errors) or null.
getChannelreadpublic tokenchannel getOne help center channel with its collections and their articles in a single request (the last 100 of each), or null.
listChannelsreadpublic tokenchannel listThe workspace's help center channels. isDefault marks the one the workspace treats as its main channel — a caller that has no channel id configured should prefer it over the first row.
updateChannelwriteBearerchannel updateUpdate a help-center channel: { input: { id, attributes } }. Returns the payload (with errors) or null.

Types: Channel

CLI:

Entitylistgetcreateupdatedelete
channel✓✓✓✓✓

On this page