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.