Getting Started with Contentful rich text and .NET
Pre-requisites
This tutorial assumes you understand the basic Contentful data model as described in the developer center and that you have already read and understand the getting started tutorial for the .NET SDK.
Contentful.net is built on .NET Core and targets .NET Standard 2.0. The SDK is cross-platform and runs on Linux, macOS and Windows.
What is rich text?
Rich text is a new JSON format for handling complex content structures in a strongly typed manner. It is represented by the rich text field in Contentful.
Working with a rich text property in the Contentful .NET SDK
Create a property in your model class of type Document
.
public Document Body { get; set; }
The Document
class contains a list of IContent
which contains all the nodes of your rich text field deserialized into strongly typed classes. When you've fetched your entry that contains the rich text field you can iterate through this collection and use pattern matching to decide which action to take depending on the type of node.
var entry = await _client.GetEntries<SomeModel>();
foreach(IContent node in entry.Body.Content) {
switch(node) {
case Paragraph p:
// Do something with the paragraph
case Heading h:
// Do something with the heading
...
}
}
The most common use case is that you need to convert your rich text into HTML. The .NET SDK provides the HtmlRenderer
class to facilitate this.
var entry = await _client.GetEntries<SomeModel>();
var htmlRenderer = new HtmlRenderer();
var html = htmlRenderer.ToHtml(entry.Body);
The html
variable will now contain an HTML string representation of the content. The HtmlRenderer
contains a list of renderers that can handle all of the node types that exist natively in the Contentful Web App, except entries. If you wish to render entries or other custom node types you need to write your own IContentRenderer
and add it to the HtmlRenderer
via the AddRenderer
method. The following is an example of a custom renderer that will render any embedded-entry-block
.
public class CustomContentRenderer : IContentRenderer
{
public int Order { get; set; }
public bool SupportsContent(IContent content)
{
return content is EntryStructure && (content as EntryStructure).NodeType == "embedded-entry-block";
}
public string Render(IContent content)
{
var model = (content as EntryStructure).Data.Target as SomeCustomModel;
var sb = new StringBuilder();
sb.Append("<div>");
sb.Append($"<h2>{model.Title}</h2>");
sb.Append("</div>");
return sb.ToString();
}
}
This custom renderer can now be added to the HtmlRenderer
.
var entry = await _client.GetEntries<SomeModel>();
var htmlRenderer = new HtmlRenderer();
htmlRenderer.Add(new CustomContentRenderer() { Order = 10 });
var html = htmlRenderer.ToHtml(entry.Body);
Note that the Order
of the renderer is set to 10 to make sure it is added earlier in the rendering pipeline. The default order of the standard renderers are 100, except for the NullContentRenderer
which has a default order of 500.
Working with a rich text property in an ASP.NET Core application
If you're working with ASP.NET Core the HtmlRenderer
gets injected into the dependency injection pipeline and is available once you run services.AddContentful(Configuration)
in your startup.cs
.
There's also a tag helper that you can use directly in your Razor files to render a Document
property to HTML.
<contentful-rich-text document="@Model.Body" ></contentful-rich-text>
The tag helper will use the configured HtmlRenderer
to render the provided Document
directly to HTML.
If you have custom nodetypes or need to render entries, some additional setup is required to add the custom renderers to the HtmlRenderer
pipeline.
services.AddTransient((c) => {
var renderer = new HtmlRenderer();
renderer.AddRenderer(new CustomContentRenderer() { Order = 10 });
return renderer;
});
It's important to add any custom renderers after your services.AddContentful(Configuration)
call and not before.
If you want your custom renderer to render a .cshtml view you can let your custom renderer inherit from the BaseContentRenderer
. This gives your renderer the RenderToString
method which takes the name of a view and a model to pass to the view.
public class CustomViewContentRenderer : BaseContentRenderer
{
public CustomViewContentRenderer(IRazorViewEngine razorViewEngine, ITempDataProvider tempDataProvider, IServiceProvider serviceProvider) : base(razorViewEngine, tempDataProvider, serviceProvider)
{
}
public override bool SupportsContent(IContent content)
{
return content is SomeModel;
}
public override string Render(IContent content)
{
var model = content as SomeModel;
return RenderToString("SomeView", model);
}
}
As there are some services required to render a .cshtml view you also need to update the creation of the renderer in your startup.cs file.
services.AddTransient((c) => {
var renderer = new HtmlRenderer();
renderer.AddRenderer(new CustomViewContentRenderer(c.GetService<IRazorViewEngine>(), c.GetService<ITempDataProvider>(), c) { Order = 10 });
return renderer;
});
Working with custom node types
If you have completely custom node types in your structure you will need to pattern match on the CustomNode
type or write a custom renderer that handles the CustomNode
type.
var entry = await _client.GetEntries<SomeModel>();
foreach(IContent node in entry.Body.Content) {
switch(node) {
case Paragraph p:
// Do something with the paragraph
case Heading h:
// Do something with the heading
case CustomNode c:
// The c.JObject property will contain JObject representation of your custom node that you can serialize into an appropriate type.
var model = c.JObject.ToObject<SomeModel>();
// Do something with the model.
...
}
}
A renderer to handle a custom node would also need to handle the CustomNode
type and then deserialize the JObject
into an appropriate type.
public class CustomViewContentRenderer : BaseContentRenderer
{
public CustomViewContentRenderer(IRazorViewEngine razorViewEngine, ITempDataProvider tempDataProvider, IServiceProvider serviceProvider) : base(razorViewEngine, tempDataProvider, serviceProvider)
{
}
public override bool SupportsContent(IContent content)
{
return content is CustomNode && (content as CustomNode).JObject["nodeType"].Value == "your-custom-node";
}
public override string Render(IContent content)
{
var model = (content as CustomNode).JObject.ToObject<SomeModel>;
return RenderToString("SomeView", model);
}
}
If you want to inject your custom renderer into the HtmlRenderer
available out of the box you also need to make sure the serialization engine knows how to deserialize your content type into a c# class. This is done by implementing an IContentTypeResolver
and instructing the engine which type a certain content type would correspond to. Here is an example.
public class EntityResolver : IContentTypeResolver
{
public Dictionary<string, Type> _types = new Dictionary<string, Type>()
{
{ "person", typeof(Person) },
{ "product", typeof(Product) },
};
public Type Resolve(string contentTypeId)
{
return _types.TryGetValue(contentTypeId, out var type) ? type : null;
}
}
This resolver then needs to be set on the client before fetching content.
_client.ContentTypeResolver = new EntityResolver();
var entry = await _client.GetEntries<SomeModel>();
var htmlRenderer = new HtmlRenderer();
var html = htmlRenderer.ToHtml(entry.Body);
The classes you expect to be part of a rich text structure must also implement the IContent
interface. This means that any class you setup with the IContentTypeResolver
should also ideally implement the IContent
interface, which is a simple marker interface used to identify parts of a rich text structure.
Next steps
Not what you’re looking for? Try our FAQ.