@ebitex/content-sdk
Consume published ebitex Content from your own site: a typed client for the Content Delivery API and React utilities that render a delivered page by dispatching each Presentation to a component you provide.
Two entry points:
@ebitex/content-sdk— the clients.createDeliveryClientis one method per endpoint;createContentClientadds site/locale/context resolution, a resolve strategy, caching and cancellation, andresolveLocation(location)to call on every router change. No React.@ebitex/content-sdk/react—<ContentProvider>,<Experience>,<RenderPresentation>,<PresentationList>,<RichText>,<Resolve>, and a guiding fallback for every Presentation that cannot be rendered. React is a peer dependency, never bundled.
Why use this instead of calling the API yourself
A site that writes its own fetch code against the Delivery API owns everything below, and none of it
shows until something breaks. This package is the maintained version of all of it:
- Every endpoint, typed, kept in step with the API: path resolution, components, streams and facets, media and response parsing.
- Sitemaps.
buildSitemapDocuments(@ebitex/content-sdk/sitemap) shards at the protocol limits, writes the index and emitshreflangalternates, so you do not join URLs by hand. - Live preview and click-to-edit.
@ebitex/content-sdk/previewis the receiver, overlay and pins. It needs the annotated responses of a preview session, which a client that parses only the fields it renders throws away. - Locale addressing. Addresses arrive already carrying their language under the path-prefix and locale-host strategies. A client that adds its own prefix doubles it.
- Caching and references. A cache with the right keys, and
resolvehandling so a delivered Presentation is never a hollow reference.
Moving a hand-written client over can be done a step at a time, keeping your own renderers: see
Replace a hand-written client.
For .NET, use Ebitex.Content.Delivery.
A browser-safe delivery key (one with an allowed-origins or allowed-IP-ranges restriction) may ship in a bundle. A key with neither restriction is a server-side key and must never reach a browser. Create either in Content → Delivery.
Install
npm install @ebitex/content-sdk
Quick start
import { createContentClient } from '@ebitex/content-sdk'
import { ContentProvider, Experience, renderersFromGlob } from '@ebitex/content-sdk/react'
const content = createContentClient({ apiKey: import.meta.env.VITE_CONTENT_DELIVERY_KEY })
const renderers = renderersFromGlob(import.meta.glob('./presentations/*.tsx'))
export function App() {
const location = useLocation()
const navigate = useNavigate()
return (
<ContentProvider client={content} renderers={renderers} markdown={Markdown}>
<Experience path={location.pathname} onRedirect={(to) => navigate(to, { replace: true })} />
</ContentProvider>
)
}
// presentations/hero.tsx — one file per Template, named by its externalId, default export
import type { PresentationRenderer } from '@ebitex/content-sdk/react'
const Hero: PresentationRenderer<{ headline: string }, { theme: string }> = ({ settings, component }) => (
<section data-theme={settings.theme}>
<h1>{component.content.headline}</h1>
</section>
)
export default Hero
A Template with no renderer, an unpublished reference, a binding that needs server-side resolution — each renders a panel naming the reason and the fix, so a missing piece is visible, never silent.
Media from the asset library
An image, video or document picked from your asset library arrives as an expanded reference. A field
may allow several kinds, and they all share one shape. isAssetValue recognizes the reference (by
its Contract's externalId: ebitex-image, ebitex-video or ebitex-document), and the content's
own kind then narrows what file holds:
import { isAssetValue } from '@ebitex/content-sdk'
function Media({ value }: { value: unknown }) {
if (!isAssetValue(value)) return null
const { content } = value
switch (content.kind) {
case 'image':
return <img src={content.file.url} width={content.file.width} height={content.file.height} alt={content.alt ?? content.name} />
case 'video':
// `file.url` is an HLS manifest: hand it to a player that plays HLS.
return content.file.posterUrl ? <img src={content.file.posterUrl} alt={content.name} /> : null
case 'document':
return <a href={content.file.url}>{content.name}</a>
}
}
file is already resolved to one locale: the rendition for the locale you asked for, falling back
along your locale tree to the default. A private asset's URL is signed and expires, so render it
and let it go rather than storing it.
Full guide and reference: developers.ebitex.io.
Worked examples: the ebitex samples repository has two complete Northwind Coffee sites built on this package, one static and one server-rendered, each with the content model it expects.
Know the visitor's audiences
Personalized and restricted content depends on which audiences a visitor is in. Give the client an audience option and it
does the whole flow: it asks which audiences your content uses, asks Audience which of them the visitor is in, keeps the
signed answer in memory, renews it, and sends it with every read.
import { createContentClient } from '@ebitex/content-sdk'
const content = createContentClient({
apiKey: import.meta.env.VITE_DELIVERY_KEY,
audience: { apiKey: import.meta.env.VITE_AUDIENCE_DELIVERY_KEY },
})
A Forms submission, a sign-in link followed in another tab, the tab coming back after a pause and a stale token each
refresh the answer by themselves, and <Experience> re-reads when it changes. useAudienceChange() re-renders your own
component when the visitor's audiences change, and useSignIn() runs the emailed-link sign-in. On a server, make one
client per request with content.forVisitor({ visitor, consent }). Audiences are never sent in the context; Content
refuses them there.
An Audience delivery key is its own kind (a Content delivery key does not work). One with an allowed-origins list is
browser-safe and the list is checked on every request. visitor and consent are optional: without them, or without
consent granted on that device, the context alone decides. It never throws into a page: when Audience cannot answer, the visitor holds no audiences and onError hears about it.