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.
Run it
Section titled “Run it”The command needs a delivery key and an output path:
npx content-sdk-codegen --key <your server-side key> --out ./src/content-types.d.tsUse 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:
export CONTENT_DELIVERY_KEY=<your server-side key>export CONTENT_DELIVERY_TYPES_OUT=./src/content-types.d.tsnpx content-sdk-codegenThat 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.
What the file contains
Section titled “What the file contains”For a small model, the output looks like this:
// 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:
statementbecomesStatement. 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 isComponentValue<Image>, a Presentation field isPresentationEnvelope<Statement>, and the file imports exactly the names it uses from@ebitex/content-sdk. So a generated type goes straight into<RichText>,<PresentationList>andPresentationRendererwith 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.
Which responses the types describe
Section titled “Which responses the types describe”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.
Use the types in a renderer
Section titled “Use the types in a renderer”Pass a generated type to PresentationRenderer, and component.content is typed by it:
// 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 FeatureWhen to regenerate
Section titled “When to regenerate”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
prebuildscript, 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.