Skip to content

Generate types from your content model

A renderer reads a Contract’s fields, and TypeScript can only check those reads against a description of the Contract. You can write that description by hand, but it drifts from the model. Rename a field in Content and your code keeps compiling against the old name, because nothing connects the two.

content-sdk-codegen connects them. It ships with @ebitex/content-sdk, reads the schemas the Content Delivery API publishes for your organization, and writes one TypeScript file describing every published Contract exactly as delivery sends it.

The command needs a delivery key and an output path:

Terminal window
npx content-sdk-codegen --key <your server-side key> --out ./src/content-types.d.ts

Use a server-side delivery key, created in Settings → API Keys with Browser-safe key turned off. A browser-safe key only answers requests from the origins you listed, and a terminal sends no Origin header, so the request is refused with origin_denied.

Each flag can come from an environment variable instead:

Flag Environment variable Required
--key CONTENT_DELIVERY_KEY Yes
--out CONTENT_DELIVERY_TYPES_OUT Yes
--url CONTENT_DELIVERY_URL No. Defaults to https://api.ebitex.io

A flag wins over its variable. The command reads the environment it runs in, not a .env file, so export the variables in your shell or set them where your build runs:

Terminal window
export CONTENT_DELIVERY_KEY=<your server-side key>
export CONTENT_DELIVERY_TYPES_OUT=./src/content-types.d.ts
npx content-sdk-codegen

That makes it easy to run from an npm script:

{
"scripts": {
"codegen": "content-sdk-codegen"
}
}

The command fetches once, writes the file, and exits. It does not watch anything or write anywhere else. The schemas it reads are the ones GET /schemas returns.

For a small model, the output looks like this:

src/content-types.d.ts
// Generated by `content-sdk codegen` — do not edit by hand.
// Reflects the last published content when this was run; re-run after publishing to refresh.
// Describes SERVER-RESOLVE delivery: requests that send a `ctx` bag and do not set `raw`.
// A client-resolve request (no `ctx`) receives the unresolved variant-set envelope for every
// personalizable field, and `raw=true` the locale envelope for every localizable one — neither
// of which these types describe.
import type { BlobValue, ComponentValue, PresentationEnvelope, RichTextValue } from '@ebitex/content-sdk'
/** image (5b0e6c1d-2f3a-4b8e-9c71-0a4d2e6f8b13@1) */
export type Image = {
alt: string
file: BlobValue
}
/** page (9e41d7a2-6c05-4f1b-8d3e-27b5c0a9f4e6@3) */
export type Page = {
title: string
description?: string
sections?: Array<PresentationEnvelope<Statement>>
}
/** statement (c3a8f2e0-1b7d-4e95-a6c4-8f02d1e7b395@2) */
export type Statement = {
heading: string
body?: RichTextValue
image?: ComponentValue<Image>
}
  • One type per published Contract, named after the Contract’s external id in PascalCase: statement becomes Statement. The comment above each type holds the Contract’s id and version in your organization.
  • Only published Contracts. A Contract nothing has published yet has nothing delivered to describe, so it has no type.
  • Keys are field external ids. A mandatory field is required and any other field is optional. A key that is not a plain identifier, such as tasting-notes, is quoted.
  • Values are this SDK’s own types. A RichText field is RichTextValue, a Component field is ComponentValue<Image>, a Presentation field is PresentationEnvelope<Statement>, and the file imports exactly the names it uses from @ebitex/content-sdk. So a generated type goes straight into <RichText>, <PresentationList> and PresentationRenderer with no casting.
  • Type aliases, not interfaces. That is deliberate: an interface is not assignable to the SDK’s generic content type, and an alias is.
  • The same Contract at two published versions produces two types. The latest keeps the plain name (Coffee) and the older one carries its version (CoffeeV1).

Types are sorted by name, so regenerating after a publish only changes the lines for what actually changed.

The header says the types describe server-resolve delivery. That is what createContentClient always gets, because it sends a personalization context with every request, so for the high-level client you can ignore the note.

It matters only if you call the Delivery API another way. A request without a context receives a personalizable field as its unresolved variants, and a request with raw=true receives a localizable field as all of its locales. Neither shape is in these types.

Pass a generated type to PresentationRenderer, and component.content is typed by it:

src/presentations/feature.tsx
// A renderer typed by the generated file. `Statement` is the `statement` Contract as your
// organization last published it, so renaming a field there and regenerating turns every read of
// the old name into a compile error.
import { isResolved } from '@ebitex/content-sdk'
import { RichText, type PresentationRenderer } from '@ebitex/content-sdk/react'
import type { Image, Statement } from './generated-content-types.js'
const Feature: PresentationRenderer<Statement> = ({ component }) => {
const { heading, body, image } = component.content
return (
<section>
<h2>{heading}</h2>
<RichText fragments={body} />
{/* A referenced image may not have resolved (it may be unpublished), so check before reading it. */}
{isResolved<Image>(image) ? <img src={image.content.file.url} alt={image.content.alt} /> : null}
</section>
)
}
export default Feature

After publishing a change to the content model. The schemas describe what was last published, not what is being edited, so a Contract you are still changing keeps its published shape until you publish it. Nothing tells you the file is out of date: regenerate whenever a Contract change goes live.

Commit the file or ignore it, as suits your project:

  • Committed, the project builds without a key, and each model change shows up as a reviewable diff. Regenerating against a different organization changes the id and version comments above each type, and nothing else.
  • Ignored, run the command before each build, for example from a prebuild script, with the key in your build environment.

Both northwind-coffee and northwind-coffee-ssr in the samples repository commit a generated content.d.ts, and northwind-coffee’s npm run codegen shows one way to run the command with a key read from .env.