CLI
npm install -D @blokcms/cli
npx blokcms login --token blok_pat_… # Account → Access tokens
Or set BLOKCMS_TOKEN (and BLOKCMS_API_URL) in CI.
| Command | |
|---|---|
blokcms spaces | List your organization's spaces. |
blokcms schema pull --space spc_… [--out blokcms.schema.ts] | Write the content model (types, option lists, locales and forms) to blokcms.schema.json, or a .ts file. Form secrets come out as environment variables, never as values. |
blokcms schema push --space spc_… blokcms.schema.ts [--prune] [--dry-run] [--allow-destructive] | Create and update types from code, after printing what each change does to content. --prune deletes types missing from the file, and the forms and lists missing from those sections (not forms with submissions); changes that delete or lossily convert values, or make published entries invalid, need --allow-destructive. |
blokcms types generate --space spc_… --out src/blokcms.d.ts | TypeScript types for your content. |
blokcms migrate --space spc_… migrations/001.ts [--dry-run] | Run a content migration. |
blokcms export --space spc_… --out space.ndjson | Export everything (runs as a background job). |
blokcms import --space spc_… space.ndjson | Import an export (upserts by id). Files come along: from the same organization directly, from anywhere else (another environment) through the download URL each export carries. |
blokcms assets import --space spc_… <https URL or file>… [--folder afd_…] | Add files to the media library. URLs are downloaded by BlokCms (https, public websites, up to 100 MB); files it already has (same content) aren't added twice. |
Schema as code
The helpers type-check every field and setting. Field types and settings are described in Modeling content.
import { defineSchema, defineType, field } from "@blokcms/cli";
export default defineSchema({
types: [
defineType({
key: "page", kind: "content", name: "Page",
fields: [
field.text("title", "Title", { required: true, translatable: true }),
field.blocks("body", "Body", { types: ["hero"], initial: [{ type: "hero" }] }),
field.seo("seo", "SEO", { tab: "SEO" }),
],
}),
defineType({
key: "hero", kind: "block", name: "Hero", titleField: "headline",
fields: [
field.text("headline", "Headline", { required: true }),
field.asset("images", "Images", { multiple: true, accept: ["image/"] }),
],
}),
],
});
Besides types, a schema file can hold what a site's code depends on: lists (option lists, with all their options), locales (added or updated, never removed; the default stays as created) and forms. Each section is only touched when the file has it. Secrets don't belong in the file: read them from the environment.
export default defineSchema({
types: [/* … */],
locales: [{ code: "fr-BE", name: "Français", fallback: "nl-BE" }],
lists: [{ key: "regions", name: "Regions", items: [{ value: "north", label: "North" }] }],
forms: [{
key: "contact", name: "Contact",
fields: [{ key: "email", label: "Email", type: "email", required: true }],
settings: { successMessage: "Thanks!", turnstileSiteKey: "0x4AAA…", turnstileSecret: process.env.TURNSTILE_SECRET },
}],
});
Form settings are merged: one left out keeps its value, so a secret whose variable isn't set stays as it is. In JSON files, write { "$env": "TURNSTILE_SECRET" } instead; schema pull does that for every secret that is set (BLOK_FORM_<KEY>_TURNSTILE_SECRET). Menus aren't part of the schema: they're content, and editors own them.
schema pull writes every field's id. Keep it when you rename a field: the field's content moves to the new key. In a file written by hand, add renamedFrom: "oldKey" to the renamed field instead. A field you leave out is deleted with its values, so schema push asks for --allow-destructive.
Content migrations
import { defineMigration } from "@blokcms/cli";
export default defineMigration(async ({ entries, getEntry, saveDraft, log }) => {
for await (const summary of entries("article")) {
const entry = await getEntry(summary.id);
const fields = { ...entry.draft.fields, subtitle: entry.draft.fields.teaser };
await saveDraft(entry.id, entry.version, { ...entry.draft, fields }, "Copy teaser to subtitle");
log(`updated ${summary.path}`);
}
});
Seeds and migrations from another CMS have two more helpers: uploadAsset(urlOrPath, { alt, folderId }) adds a file (by URL or from disk) and returns { asset, duplicate } without storing a file twice, and blockId(seed) gives a block the same id on every run, so an unchanged entry isn't saved again. The Management API finds entries by path too: GET /v1/spaces/spc_…/entries/by-path/blog/hello (drafts included).
export default defineMigration(async ({ uploadAsset, blockId, api, spaceId }) => {
const { asset } = await uploadAsset("https://old-site.example.com/images/team.jpg", { alt: "Our team" });
const page = await api.get(`/spaces/${spaceId}/entries/by-path/about`);
// … a block { id: blockId("about/team"), type: "image", fields: { image: { asset: asset.id } } }
});