// A content type declared as CODE — typesafe, version-controlled, PR-reviewed. // // Each field uses the CMS `Schema` field DSL (distinct from the @voltro/database // column DSL). `defineContentType` resolves this to a descriptor the editor // introspects to generate its form, and `contentTypeToEntities` (in // database/schema.ts) compiles the fields DOWN to real `blogPost_drafts` + // `blogPost_published` columns. // // Imported from the MAIN `@voltro/cms` entry because it runs server-side at // discovery time (schema.ts imports it). The browser-safe field DSL is the same // object either way — the editor page never imports this file; it fetches the // resolved descriptor over rpc (content.types). import { Schema, defineContentType, derivedFrom, slugify } from '@voltro/cms' export const blogPost = defineContentType({ name: 'blogPost', displayName: 'Blog post', pluralName: 'Blog posts', fields: { // Single-line text, capped at 200 chars (validated at save time). title: Schema.String.pipe(Schema.maxLength(200)), // Computed-on-save from `title` (a caller value is overwritten), constrained // to url-safe chars, and UNIQUE per tenant — the derived tables get a real // composite (tenantId, slug) DB UNIQUE. slug: Schema.String.pipe( Schema.pattern(/^[a-z0-9-]+$/), Schema.unique(), derivedFrom('title', slugify), ), // Rich text stored as a JSON document (e.g. a TipTap doc). Images allowed. body: Schema.RichText({ allowImages: true }), // Optional short summary → nullable column + non-required editor input. excerpt: Schema.String.pipe(Schema.maxLength(280)).optional(), // Optional publish date the consumer app can order/filter by. publishedAt: Schema.DateTime.optional(), }, // Which columns the editor's list view shows + its default sort. list: { columns: ['title', 'slug', 'status'], defaultSort: { field: 'updatedAt', dir: 'desc' } }, })