Skip to main content

Rich text and embedded entities

VoxRichText is a first-class, immutable property type. Vox stores one canonical strict-HTML representation and can deliver either that HTML or a semantic JSON document. Unlike a string with [UIHint("html")], the value is validated, its embedded references are modeled explicitly, and storage keeps a separate reference index.

public virtual VoxRichText Description { get; set; } = VoxRichText.Empty;

Create a value with VoxRichText.Parse(html), or with VoxRichText.FromDocument(document). Input must be well-formed XML-style HTML. Vox accepts its documented formatting elements and safe link attributes only; unsupported elements, malformed nesting, event/style attributes, and unsafe URL schemes fail validation rather than being silently repaired. Ordinary links use <a>. Embeddings use a dedicated empty element:

<vox-embed ref="vox://embed/product-card/p123"></vox-embed>

The URI identifies an embedding contract and target entity ID, not an entity type. Referenced entities may be missing. The embedding name must be registered.

Define an embedding contract​

An embedding deliberately maps one entity to a safe output model. It never serializes the entity itself:

public record ProductCard(string Name, string Url, VoxRichText Teaser);

[VoxEmbedding("product-card", "1")]
public class ProductCardEmbedding : IVoxEmbedding<Product, ProductCard>
{
public async Task<ProductCard?> EmbedAsync(
Product product,
IVoxCalculationContext context,
CancellationToken cancellationToken)
{
var categories = await context.LoadRelatedAsync<Category>(product, cancellationToken);
return new ProductCard(product.Name, $"/products/{product.Id}", product.Teaser);
}
}

The output contract may contain nested rich text, whose embeds are resolved into the same flat graph. Entity types are rejected in output contracts so an embedding cannot accidentally expose every property. Reads through IVoxCalculationContext are dependency-tracked just like calculations and read projections.

Resolution memoizes each reference and has depth, reference-count, and output-size limits. Cycles and missing targets produce diagnostics; they never cause recursive serialization. Change the embedding version when the output contract or mapping changes.

API delivery​

Entity and relation reads accept:

?richTextFormat=Html|Json&expandEmbeds=true|false

Html is the default and returns each value as canonical HTML. Json returns a versioned semantic document with typed nodes such as paragraph, heading, link, and embed. With expansion enabled, the entity receives a flat _embedded object keyed by the canonical vox://embed/... URI. _embedDiagnostics describes missing, cyclic, or bounded references. Nested rich text inside embedding output uses the selected delivery format too.

Expansion uses the request's content scope. A draft or release therefore embeds the corresponding draft/release version of every entity it reads. For content=Both, the published and draft halves are expanded independently.

Reference index​

Vox extracts references when an entity is written and persists them separately from ordinary relations. Each row records content mode/layer, source entity type and ID, property path, varying context key, embedding name, and target ID. IVoxRichTextReferenceIndex provides outgoing and incoming lookups. Applications that need relationships never have to parse stored HTML, and published, draft, and release references cannot leak into one another.

Dynamic schema fields can use RichText. Rich text is not filterable, sortable, unique, pattern-constrained, or a string/list-length field. Editor metadata exposes the richText editor automatically.