A help center has three kinds of page: an index of categories with a search box, a page per category, and the article itself with its breadcrumbs, table of contents and a "Was this article helpful?" vote. @kookee/react has the components for all three. The examples use the Next.js App Router, with the SDK client and its config from lib/kookee.ts; see Using the React components with Next.js, TanStack Start or Vite for that file and for other setups.
The index page
// app/help/page.tsx
import { HelpCategoryGrid } from '@kookee/react/help';
import { HelpSearchBox } from '@kookee/react/help/client';
import { kookee, kookeeConfig } from '@/lib/kookee';
export default async function HelpPage() {
const categories = await kookee.help.categories();
return (
<>
<HelpSearchBox config={kookeeConfig} href="/help/{categorySlug}/{slug}" />
<HelpCategoryGrid categories={categories} href="/help/{categorySlug}" />
</>
);
}
HelpCategoryGrid renders a card per category with its icon, name, description and article count. The grid fills the width with cards at least --kookee-card-min wide, 17rem unless you change it.
Search as you type
HelpSearchBox searches your published help articles by meaning, not keywords, and lists the matches under the input, each with the passage that matched.
It searches from two characters, 300 ms after typing stops, and aborts a search still in flight when the query changes.
It announces loading, the number of results and errors to screen readers.
searchLocaleis the content locale to search in.localeonly formats the result count.query,defaultQueryandonQueryChangekeep the query in sync with the URL.showCategory(from version 1.9.3) prints each result's category name above its title.onResultClickis called with the result a visitor opened, which is useful for analytics. It is a function, so in Next.js render the box from a'use client'file of your own when you pass it.
For a search UI of your own, useHelpSearchBox({ config }) returns { query, setQuery, results, status, error } with the same debounce and cancellation. It is a different hook from the root's useHelpSearch, which needs a KookeeProvider.
The category page
// app/help/[category]/page.tsx
import { notFound } from 'next/navigation';
import { HelpArticleList, HelpBreadcrumbs, HelpCategoryHeader, HelpCategoryNav } from '@kookee/react/help';
import { kookee } from '@/lib/kookee';
export default async function CategoryPage({ params }: { params: Promise<{ category: string }> }) {
const { category: slug } = await params;
const [categories, articles] = await Promise.all([
kookee.help.categories(),
kookee.help.list({ category: slug, limit: 100 }),
]);
const category = categories.find((c) => c.slug === slug);
if (!category) notFound();
return (
<div className="help-layout">
<HelpCategoryNav categories={categories} activeCategorySlug={slug} href="/help/{categorySlug}" showCounts />
<main>
<HelpBreadcrumbs homeHref="/help" category={category} />
<HelpCategoryHeader category={category} />
<HelpArticleList articles={articles.data} href="/help/{categorySlug}/{slug}" />
</main>
</div>
);
}
HelpCategoryNavlists every category as a link, with its icon and, withshowCounts, its article count. The active one getsaria-currentanddata-active="true".HelpCategoryHeaderrenders the icon and name as the page heading, the description, and the article count unless you passshowCount={false}.HelpBreadcrumbsrenders "Help Center / Category" as an ordered list inside a<nav>. The last crumb is the current page, unlinked, witharia-current="page".beforeadds crumbs in front, such as your site's home page:before={[{ label: 'Home', href: '/' }]}, andseparatorreplaces the/between them.HelpArticleListrenders a card per article with its title and excerpt. Given search results instead, it shows the passage that matched. For a list that mixes categories, such as popular articles on the index page, render each item as<HelpArticleCard article={item} href="/help/{categorySlug}/{slug}" showCategory />throughrenderItem:showCategory(from version 1.9.3) prints the category name above the title, as text.
The article page
// app/help/[category]/[slug]/page.tsx
import { HelpArticle, HelpBreadcrumbs } from '@kookee/react/help';
import { HelpfulVote } from '@kookee/react/help/client';
import { extractHeadings } from '@kookee/react/entry';
import { TrackedTableOfContents } from '@kookee/react/entry/client';
import { kookee, kookeeConfig } from '@/lib/kookee';
export default async function ArticlePage({ params }: { params: Promise<{ slug: string }> }) {
const article = await kookee.help.getBySlug((await params).slug);
return (
<>
<HelpBreadcrumbs homeHref="/help" category={article.category} categoryHref="/help/{categorySlug}" article={article} />
<HelpArticle article={article} classNames={{ content: 'prose' }} />
<TrackedTableOfContents headings={extractHeadings(article.contentHtml)} offset={64} resetKey={article.id} />
<HelpfulVote
entryId={article.id}
reactions={article.reactions}
config={kookeeConfig}
storageKey="kookee:helpful-votes"
showCounts
/>
</>
);
}
HelpArticle renders the title, an "Updated" line with the date the article last changed, and the body. From version 1.9.3 it takes headerClassNames for the header's title and updated elements, and HelpArticleViews renders the article's view count, such as "42 views", wherever you place it:
import { HelpArticle, HelpArticleViews } from '@kookee/react/help';
<HelpArticle article={article} classNames={{ content: 'prose' }} headerClassNames={{ title: 'text-3xl' }} />
<HelpArticleViews article={article} />
The views label translates the count, with one form per plural category: labels={{ views: { one: '{count} vue', other: '{count} vues' } }}.
Handle a missing slug with notFound(), as in Build a blog with the React components, and see there for the table of contents' offset and resetKey.
"Was this article helpful?"
HelpfulVote asks the question with a yes and a no button. Votes are stored as the article's yes and no reactions, so you can read them back from article.reactions through the API.
A visitor has one vote. Choosing the other answer removes the first.
The visitor's vote is remembered in
localStorage. Give it its ownstorageKey, as above, when the site also showsReactions, so the two widgets do not share their memory.showCountsshows how many people chose each answer. Leave it out to hide the numbers.The root carries
data-vote, which isyes,noornone, so you can show a thank-you message with CSS alone.iconsadds an icon before each answer, and thehelpfulQuestion,helpfulYesandhelpfulNolabels replace the text.
Like reactions, votes are anonymous. They tell you which articles are working, not exactly how many people agree.
Other articles in the category
A sidebar of the category's other articles is the same list with a smaller item:
import { HelpArticleList, HelpArticleTitle } from '@kookee/react/help';
const siblings = await kookee.help.list({ category: article.category?.slug, limit: 20 });
<HelpArticleList
articles={siblings.data.filter((a) => a.id !== article.id)}
renderItem={(item) => <HelpArticleTitle article={item} href="/help/{categorySlug}/{slug}" headingLevel={3} />}
/>;
Loading states
HelpCategoryGridSkeleton and HelpArticleListSkeleton render the grid's and the list's own layout with placeholders, so the page keeps its shape while content loads.
Pair it with the AI chat
The components cover reading and searching. For questions your articles answer only in part, add the AI chat widget, which answers from the same published articles and links to them: see Building a help center with search and AI chat.