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.
Write the types
Section titled “Write the types”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.
use sam_sdk::{ContentRef, ContentType, RichText};use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Clone, PartialEq, ContentType, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]#[content_type( id = "2066d4d5-0dff-40c0-b42b-cc35e694fa73", base = "page", display_name = "Article", description = "A news or blog article.")]#[allowed_children()]pub struct ArticlePage { /// Summary. #[field(localized, order = 10, editor = "textarea")] pub summary: Option<String>, /// Image. #[field(order = 20, editor = "image")] pub image: Option<ContentRef>, /// Main body. #[field(localized, order = 30)] pub main_body: Option<RichText>, /// Shared across languages (not localized). #[field(order = 40)] pub author: Option<String>,}The crate is sam-sdk. Rust field names become PascalCase in the manifest (main_body → MainBody), so a Rust and a C# model of the same type derive the same field IDs.
What the attributes mean
Section titled “What the attributes mean”| 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.
Field kinds
Section titled “Field kinds”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.
Field IDs and renames
Section titled “Field IDs and renames”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.
Build and push the manifest
Section titled “Build and push the manifest”seeamess-schema build --assembly <path.dll> --out manifest.jsonseeamess-schema push --manifest manifest.json --url http://127.0.0.1:5080 --token devThe Rust SDK builds the manifest in code: Manifest::new().with::<StartPage>().with::<ArticlePage>()… and to_json_pretty(). The sample crate writes it to a file; the engine binary pushes it, no SDK needed.
SEEAMESS_CARGO_DIR=sdk/rust scripts/cargo run -p site-models -- --out manifest.jsonseeamess schema push --manifest manifest.json --url http://127.0.0.1:5080 --token devBoth 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 plan
Section titled “The plan”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.
Porting an Optimizely model
Section titled “Porting an Optimizely model”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.