Skip to main content

Best Practices

Hard-won guidance from real Krios projects.

Naming

  • Name a field once. Renaming apiName is unsupported — the field is the contract. To rename, create a new field, migrate values, soft-delete the old one. Same goes for content types.
  • camelCase apiNames. articlePage, not article_page or ArticlePage. The codegen + GraphQL surfaces both expect camelCase.
  • Pluralize collections. A categories reference field, not category, when isMultiple: true. The auto-generated GraphQL field name follows.

Inheritance vs composition

  • Prefer composition over deep inheritance. Two levels covers most cases (BasePage → ArticlePage). Three levels is a smell — switch to blocks / references instead.
  • Use a single common base. A BasePage with title, slug, SEO is a strong starting point. Page-shaped types extend it; block-shaped types stay separate.

Field flags

  • Required is a publish gate, not a save gate. Drafts can be incomplete. Don't reach for isRequired to mean "I want this field filled out eventually" — that belongs in author guidance.
  • isLocalizable: false is the default for most fields. Only flip it on for content the author should be able to translate. Slugs, dates, references usually stay shared.
  • isSearchable widens the FTS index. Use it on title-like fields, not on every text field. Bigger index = slower writes.
  • isFilterable / isSortable are advisory today (they hint to UI components); plan for them to gate API behavior in V2.

Tabs

  • Tabs > nested groupings. Group fields by editor concern (Content, SEO, Settings) rather than by jamming them in big nested object structures.
  • Standard tab names. Content, SEO, Settings, Layout, Publishing are common and self-explanatory; pick from this set unless you need a project-specific bucket.
  • Don't skip tabs for short types. Even a 4-field type benefits from a single Content tab — the editor gets a consistent shell.

References

  • Required references should have a fallback. A publish gate on a required reference becomes annoying when the reference is optional in practice. Use a defaulted reference + content-type default value instead.
  • allowedTypeIds is your friend. Constrain reference fields to the types that make sense — the picker UX gets tighter, and the validator catches mistakes at save time.

Block design

  • Blocks are content, not components. A "card grid" block stores card data; the rendering is your frontend's responsibility. The CMS doesn't ship templates.
  • Don't over-fragment. A heroBlock with headline + image + cta is fine; splitting heroHeadlineBlock, heroImageBlock, heroCtaBlock is too much.
  • Reuse across pages. A single ctaBlock entry can be embedded in many landing pages. Update once; every embed reflects the change.

Validation rules

  • Use validations.regex for patterned text (phone numbers, hex colors). The CT editor exposes presets (Email, URL, Phone, Hex Color).
  • min / max on numbers rather than client-side validation only.
  • enumOptions on enums. Don't use a free-text field with documentation telling authors which values to use — make it an enum with proper labels.

Folders

  • Use folders for organization, not access control. Folder placement doesn't gate read permissions — that's an API-key / role concern.
  • _Data for non-routable content. Block entries, settings, reusable rich-text snippets all belong under _Data so the editor sees them grouped.

Common mistakes

  • Adding a title field to a child that already inherits one. The server rejects with field_inherited. Inherited fields are managed on the parent.
  • Setting isLocalizable on slug. The slug field hides the toggle in the CT editor; manual schema pushes that include it are silently ignored. Slugs are inherently per-site, not per-locale-per-site for routing simplicity.
  • Forgetting treeParentId on entry create. Every new entry takes a place in the tree. The API rejects with tree_parent_required unless you're creating into the project's root scope explicitly.