Most problems with the @kookee/react components come from a missing stylesheet, the order of CSS imports, or the boundary between server and client components. Find the symptom below.
The components have no styling
Import the components' stylesheet once, at the root of your app:
import '@kookee/react/content.css';
Without it the components render plain, unstyled markup. That is deliberate, for sites that write all of the CSS themselves, but easy to miss. Code blocks and file chips inside an entry's body come from a second stylesheet, @kookee/sdk/styles/content.css, so a page with styled cards and an unstyled code block is missing that one.
Headings, lists or images look wrong in a Tailwind app
In Tailwind v4, import both stylesheets into the components layer after @import 'tailwindcss':
@import 'tailwindcss';
@import '@kookee/sdk/styles/content.css' layer(components);
@import '@kookee/react/content.css' layer(components);
Imported before Tailwind, the components layer is declared below Preflight, and Preflight's rules for headings, ol and img win over the components' own.
My utility classes do not apply
The same fix. An unlayered import beats every layered Tailwind utility, whatever its specificity, so classNames={{ title: 'text-xl' }} does nothing until the stylesheet sits in the components layer.
A list is empty or has no width
BlogPostList, ChangelogList, HelpCategoryGrid and HelpArticleList are size containers (container-type: inline-size), which take no width from their content. As a shrink-to-fit child, such as a flex item without a width, w-fit or an auto grid track, they collapse to zero and look empty. Give the list a definite width: leave it a block, stretch it in the flex row (flex-1 or w-full), or put it in a 1fr grid track.
Every tag shows although the tag filter has a limit
The tags past limit carry the hidden attribute. A class you pass through classNames.item that sets display, such as flex, overrides hidden and shows them all. Set display on the list instead, or keep [hidden] hidden in your own CSS; Tailwind's Preflight already does.
The article body is unstyled but the rest is fine
Two causes, depending on what looks wrong.
Code blocks and file chips are plain. The SDK's content stylesheet is scoped to the kookee-entry-content class. EntryContent, BlogPost, ChangelogArticle and HelpArticle add it for you. If you render contentHtml yourself with dangerouslySetInnerHTML, add the class to that element, or render it with EntryContent.
Headings, lists and tables look like plain text. Neither stylesheet styles body text. Give the body your own typography, such as Tailwind's prose through classNames={{ content: 'prose' }}, or import @kookee/sdk/styles/typography.css.
"Functions cannot be passed directly to Client Components"
Next.js raises this when a Server Component passes a function to a client component. Reactions, HelpfulVote, TrackedTableOfContents, WhatsNew and HelpSearchBox are client components, so from a Server Component they accept only data: give them string href templates such as "/help/{categorySlug}/{slug}", not functions.
When you need a function prop, such as onReact, onResultClick, onOpenChange or renderLink, render the component from a 'use client' file of your own and pass the data into that. The server-rendered components (lists, cards, articles, filters and pagination) accept functions from a Server Component without this.
The article body arrives twice in the page
You imported EntryContent from @kookee/react, the package root, in a Server Component. The root is a client module, so Next.js renders EntryContent as a client component and also serializes its entry prop, body included, into the page. Import it from @kookee/react/entry.
The same applies to anything you pass a client component: TrackedTableOfContents takes extractHeadings(entry.contentHtml), not the entry, for this reason.
Pagination and filter links lose their query string (TanStack Router)
TanStack Router's to is a pathname, so to="/blog?page=2" goes to /blog. Pass renderLink an adapter that splits the URL into to and search; Using the React components with Next.js, TanStack Start or Vite has a copy-ready one.
"All" looks selected next to the filter I chose (TanStack Router)
By default TanStack marks every link whose path and search the current URL contains as active, so the "All" link at /changelog counts as active on /changelog?type=fix. Pass activeOptions={{ exact: true }} in your link adapter.
The table of contents highlights the wrong heading
Set
offsetto the height of your sticky header, in pixels, and give your headings ascroll-margin-topno larger than it.Pass the entry id as
resetKey. Without it, a client-side navigation from one entry to another keeps measuring the first one's headings.Only headings with an
idare listed, and nothing renders below two headings. LowerminHeadingsif short entries should still show one.
A script or listener inside the article body stops working
React 19 replaces the HTML of an element rendered with an inline dangerouslySetInnerHTML={{ __html }} whenever its parent re-renders, because it compares the object, not the string. Everything inside is rebuilt: listeners you attached from outside are gone, and elements you kept a reference to are detached. EntryContent avoids it by memoising that object, so render bodies with EntryContent, and look elements up by id or selector when you need them rather than holding on to them.
Reactions or votes forget what the visitor pressed
What a visitor pressed is kept in their browser's localStorage, under the storageKey (default kookee:reactions). It does not follow them to another browser or device, and a private window forgets it when it closes. When storage is blocked, the components remember for the rest of the page's life only.
If Reactions and HelpfulVote appear on the same site, give each its own storageKey.
Reactions show an error
The root gets data-error="true" when a request fails. Check, in order:
A custom
typeinoptionsmust match[a-z0-9_]{1,50}: lowercase letters, digits and underscores.configneeds your API key, with your site's domain in the project's Allowed Origins. AprojectIdworks only while the project's hosted portal is enabled; otherwise every request is refused with 403.The API limits reactions per IP address. A test script clicking in a loop hits it; visitors do not.
The what's-new count is missing from the server's HTML
By design. The last-seen date lives in the visitor's localStorage, which the server cannot read, so the count appears once the page has hydrated. useWhatsNew reports unseenCount as null until then.
next/image rejects the cover images
Add the Kookee media host to images.remotePatterns in next.config:
images: {
remotePatterns: [{ protocol: 'https', hostname: 'cdn.kookee.dev' }],
},
An import path does not resolve
@kookee/react exports these paths and no others:
@kookee/react,@kookee/react/chatand@kookee/react/chat.css@kookee/react/entryand@kookee/react/entry/client@kookee/react/blog@kookee/react/changelogand@kookee/react/changelog/client@kookee/react/helpand@kookee/react/help/client@kookee/react/roadmap@kookee/react/content.css
Every path except the root, /chat and /chat.css needs version 1.9.0 or later. An error such as Package subpath './blog' is not defined by "exports" means an older version is installed; update both @kookee/react and @kookee/sdk.
A missing export or prop on a path that does resolve, such as RoadmapCategoryFilter, Highlight, HelpArticleViews, ChangelogAuthor, or limit on BlogTagFilter, needs version 1.9.3.
For problems with entry bodies themselves, such as escaped HTML or broken typography, see Content renders wrong or unstyled.