Skip to content
Esc
navigateopen⌘Jpreview
On this page

@stetcms/vite

The Vite plugin that turns your content model into typed TypeScript, the file it writes, and the reason it never breaks your build.

@stetcms/vite turns your organization’s content model into a typed module in your project. The collections and maps your content team shapes become stet.<slug>.list() and .get() calls your editor autocompletes, and a field added in the UI reaches your types moments later without a restart.

It also publishes your analytics tracking plan, so the events your code declares can be charted in Stet before anyone has fired one.

The plugin

import { stet } from '@stetcms/vite';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [stet()],
});

Every option overrides the same key in stet.config.ts:

PropType
origin?string

The Stet deployment to generate from. Falls back to the config file, then STET_ORIGIN, then the hosted cloud.

Typestring
apiKey?string

Key used to fetch the model at codegen time. Falls back to STET_API_KEY. Never written into the generated file.

Typestring
output?string

Where the generated module goes, relative to the project root.

Typestring
Defaultsrc/stet.gen.ts
watch?boolean

Regenerate while the dev server runs. Never affects builds.

Typeboolean
Defaulttrue
config?string

Path to stet.config.ts. Auto-detected when omitted.

Typestring

What it does, and when

On build and dev-server start

buildStart fetches /api/v1/model and writes the generated module. If the config declares an analytics plan, it publishes that too.

While the dev server runs

Every three seconds it refetches the model and rewrites the file, but only when the rendered output actually changed — so an unchanged model triggers no reload. Set watch: false to turn this off.

The config file is re-read on every pass rather than cached, so editing it during a watch run takes effect without restarting the dev server.

It never breaks your build

This is a rule, not an implementation detail. Without a key, or with Stet unreachable, the plugin warns and carries on:

Situation What happens
No API key Warns, and writes an empty model only if no file exists yet
API unreachable or erroring Warns, and leaves the last generated file in place
Tracking plan fails to publish Warns; the bundle is produced either way
Transient failure during a watch Silent; the next tick retries

Keeping the previous file is the important half: a network blip during a deploy leaves you building against a slightly stale model, not against nothing.

The generated module

The file is safe to commit. It embeds no key, and reads both the key and the origin from the environment at runtime, so one generated file works in every environment — and a build that could not refresh it cannot pin a deployment to whatever origin the last developer generated from.

// Generated by Stet from your content model. Do not edit: it is
// regenerated by @stetcms/vite or `stet generate`.
import { createContentClient } from '@stetcms/client';
import type { ContentEntryBase, ContentAsset, ContentReference } from '@stetcms/client';

export type PostsEntry = ContentEntryBase & {
  fields: {
    body?: string | null;
    cover?: ContentAsset | null;
    /** Reference into "authors": fetch with stet["authors"].get(slug). */
    author?: ContentReference | null;
    status?: 'draft' | 'published' | null;
  };
};

export type StetContentModel = {
  posts: { kind: 'collection'; entry: PostsEntry };
};

const origin =
  typeof process === 'undefined'
    ? 'https://stetcms.com'
    : (process.env.STET_ORIGIN ?? 'https://stetcms.com');

export const stet = createContentClient<StetContentModel>({
  origin,
  apiKey: typeof process === 'undefined' ? undefined : process.env.STET_API_KEY,
});

Three things are worth knowing about the output:

  • Only used imports are emitted. A model with no asset field imports no ContentAsset, so a strict lint has nothing to complain about.
  • Reference fields carry a doc comment naming the collection they point at. The type is the same shape whatever the target, so the doc comment is where your editor tells you what to pass the slug to.
  • Type names are derived from slugs: case-studies becomes CaseStudiesEntry. A slug starting with a digit is prefixed (Stet…Entry) so the identifier stays legal.

How fields are typed

Field type TypeScript
number number
checkbox boolean
person { id: string; name: string }
select a union of the option names
multi_select an array of that union
asset ContentAsset
reference ContentReference
multi_reference ContentReference[]
text, rich_text, date, link string

A select with no options yet falls back to string rather than to never, so a half-built model still compiles. Every field is emitted as ?: T | null.

Doing it without Vite

The codegen is a plain pair of functions, exported from @stetcms/client/codegen. The CLI uses them, and so can you:

import { fetchContentModel, renderContentModule } from '@stetcms/client/codegen';

const model = await fetchContentModel('https://stetcms.com', process.env.STET_API_KEY);
const code = renderContentModule(model, 'https://stetcms.com');
PropType
fetchContentModel(origin, apiKey)?Promise<ContentModel>

Fetches the content model the given key's organization has shaped. Throws on a non-2xx response.

TypePromise<ContentModel>
renderContentModule(model, origin)?string

Renders the generated module. Pure: no filesystem, no network.

Typestring
entryTypeName(slug)?string

The type name for a slug. "case-studies" becomes "CaseStudiesEntry".

Typestring
ContentModel?type

The /api/v1/model payload: types, each with a slug, kind and fields.

Typetype

Config loading

@stetcms/vite/config is a Node-only entry point, kept out of the plugin so the CLI can reuse it without pulling in Vite:

PropType
findStetConfig(root)?string | undefined

Finds a config by trying CONFIG_CANDIDATES under root.

Typestring | undefined
loadStetConfig(path)?Promise<StetConfig | undefined>

Loads a config with jiti and returns its defineStet() result.

TypePromise<StetConfig | undefined>
trackingPlanEvents(config)?EventsShape | undefined

The plan's events, if the config declares any.

TypeEventsShape | undefined
CONFIG_CANDIDATES?string[]

The paths tried when none is given.

Typestring[]

Was this page helpful?