Skip to content

Define content types

Content types live in code. An SDK turns them into a schema manifest (JSON), and you push the manifest to the engine, which answers with a plan before anything is applied. The engine never loads your code: anything that can produce the manifest and speak HTTP can integrate, and both SDKs are optional.

Both SDKs describe the same model. These are the sample types from the repository.

using SeeAMess.Schema;
[ContentType(Id = "2066d4d5-0dff-40c0-b42b-cc35e694fa73", DisplayName = "Article", Description = "A news or blog article.")]
[AllowedChildren]
public class ArticlePage : Page
{
[Localized, Field(Order = 10, DisplayName = "Summary", Editor = Editors.Textarea)]
public string? Summary { get; set; }
[Field(Order = 20, DisplayName = "Image", Editor = Editors.Image)]
public ContentRef? Image { get; set; }
[Localized, Field(Order = 30, DisplayName = "Main body")]
public RichText? MainBody { get; set; }
/// <summary>Shared across languages (not localized).</summary>
[Field(Order = 40, DisplayName = "Author")]
public string? Author { get; set; }
}

The package is SeeAMess.Schema: attributes, the base types Page, Block and Media, and value types such as RichText, ContentRef, ContentArea and Url. At runtime the classes are plain records: no proxies, no virtual, no startup scan.

Concept C# Rust Manifest
Type ID (required, stable) [ContentType(Id = …)] #[content_type(id = …)] id
Base : Page, : Block, : Media base = "page" base: page, block, media, folder
Allowed children [AllowedChildren(typeof(…))]; empty means none #[allowed_children(…)] allowedChildren: null is any type
Translated per language [Localized] localized localized: true
Order, editor hint Field(Order = 10, Editor = …) order = 10, editor = "…" order, editor
Allowed types of a reference [AllowedTypes(typeof(…))] allowed_types(…) settings.allowedTypes

Fields without [Localized] are shared: they are stored on the master language and merged into every locale when the content is published. See Content model.

text, richText, bool, int, float, dateTime, url, reference, contentArea, json and opaque. A richText value is {"html": "…"}, a reference is {"id": "<uuid>"}, a content area is an array of {"id", "display"} items. opaque holds values imported from Optimizely that SeeAMess carries without interpreting.

Each field has a stable UUID: version 5, with the type ID as namespace and the field’s original name. Version data is keyed by field ID, so renaming a field is a schema change with no data rewrite. When you rename a C# property, mark it with [FormerlyNamed("Old")] so the generator keeps using the oldest name; [Field(Id = "…")] overrides the ID outright.

Because Optimizely import uses the same rule (type GUID plus property name), a ported model class matches imported data with no mapping file.

seeamess-schema build --assembly <path.dll> --out manifest.json
seeamess-schema push --manifest manifest.json --url http://127.0.0.1:5080 --token dev

Both push commands accept --scope <scope>, --dry-run and --force, and exit with:

Exit code Meaning
0 Applied, planned (dry run) or unchanged
2 Breaking changes were refused; push again with --force
1 Any other error, such as a scope clash or an invalid manifest

The engine matches types and fields by ID and answers with what changes:

{
"revision": 2,
"added": [{"type": "StartPage", "field": "Footer", "id": "…"}],
"changed": [{"type": "ArticlePage", "field": "Author", "id": "…", "changes": ["displayName"]}],
"removed": [],
"breaking": [{"type": "ArticlePage", "field": "Summary", "id": "…", "reason": "kind changes from text to int"}]
}

Breaking means removing a type that has content, or changing a field’s kind. Breaking changes are applied only with --force. A manifest equal to the current one writes no revision.

Change base types and attributes, and remove virtual:

Optimizely SeeAMess
PageData Page
[CultureSpecific] [Localized]
XhtmlString RichText
ContentReference ContentRef

Keep the [ContentType(GUID = …)] value as the Id, and the imported content lines up with your class.