Best Practices
Hard-won guidance from real Krios projects.
Naming
- Name a field once. Renaming
apiNameis 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, notarticle_pageorArticlePage. The codegen + GraphQL surfaces both expect camelCase. - Pluralize collections. A
categoriesreference field, notcategory, whenisMultiple: 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
BasePagewith 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
isRequiredto mean "I want this field filled out eventually" — that belongs in author guidance. isLocalizable: falseis the default for most fields. Only flip it on for content the author should be able to translate. Slugs, dates, references usually stay shared.isSearchablewidens the FTS index. Use it on title-like fields, not on every text field. Bigger index = slower writes.isFilterable/isSortableare 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,Publishingare 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.
allowedTypeIdsis 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
heroBlockwithheadline + image + ctais fine; splittingheroHeadlineBlock,heroImageBlock,heroCtaBlockis too much. - Reuse across pages. A single
ctaBlockentry can be embedded in many landing pages. Update once; every embed reflects the change.
Validation rules
- Use
validations.regexfor patterned text (phone numbers, hex colors). The CT editor exposes presets (Email, URL, Phone, Hex Color). min/maxon numbers rather than client-side validation only.enumOptionson 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.
_Datafor non-routable content. Block entries, settings, reusable rich-text snippets all belong under_Dataso the editor sees them grouped.
Common mistakes
- Adding a
titlefield to a child that already inherits one. The server rejects withfield_inherited. Inherited fields are managed on the parent. - Setting
isLocalizableonslug. 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
treeParentIdon entry create. Every new entry takes a place in the tree. The API rejects withtree_parent_requiredunless you're creating into the project's root scope explicitly.