Sanity: Serializing custom annotations in .NET

Knut from Sanity has a guide on how to create internal and external links in Sanity Studio. He also shows how you can render these links in your front end. Unfortunately, he does not show how you can do it using .NET. After digging around the github repo for Sanity LINQ I found a commit message describing how to do this.

If we inspect the page we can find the annotations under the type blocks and span. Each span might have a mark, with an id included. This id references a key in the markDefs array, which contains additional information about the annotation.

Serializing annotations using Sanity LINQ

Sanity LINQ recently released a new NuGet package (1.4.5) that allows us to create a serializer for our annotations.

To add our serialization logic we need to create a class that inherits from SanityHtmlSerializers. Inside my new serializer class, I can override the method TrySerializeMarkDef. Inside this method is where we can add our own logic.

I can now create an instance of my serializer class, and override the default block serializer in the HTML builder.


public class CustomSanitySerializer : SanityHtmlSerializers
{
    protected override bool TrySerializeMarkDef(JToken markDef, object context, ref StringBuilder start, ref StringBuilder end)
    {
        return base.TrySerializeMarkDef(markDef, context, ref start, ref end);
    }
}

public class SanityService
{
    private SanityOptions _sanityOptions;
    public SanityDataContext SanityDataContext { get; set; }
    public SanityService()
    {
        _sanityOptions = new SanityOptions
        {
            ProjectId = "<projectId>",
            Dataset = "<dataset>",
            Token = "<token>"
        };
        var customSerializer = new CustomSanitySerializer();

        SanityDataContext = new SanityDataContext(_sanityOptions);
        SanityDataContext.AddHtmlSerializer("block", customSerializer.SerializeDefaultBlockAsync);
}

Now we can implement our logic inside the TrySerializeMarkDef method. In the snippet below I have created a simple implementation to build an HTML string for the external URL. We can append our HTML to the StringBuilder start and end. Sanity LINQ will take care of the rest.

protected override bool TrySerializeMarkDef(JToken markDef, object context, ref StringBuilder start, ref StringBuilder end)
{
    var type = markDef["_type"]?.ToString();
    if (type == "externalLink")
    {
        var href = markDef["href"]?.ToString();
        var blank = (bool?)markDef["blank"] ?? false;
        var target = blank ? "target='_blank' rel='noopener'" : ""; 
        
        start.Append($"<a {target} href='{href}'>");
        end.Append("</a>");
        
        return true;
    }
}

Now we can do the same to the internal link annotation! First, to get the href to the internal page we need to dereference the internal link. The easiest way to do this is in our GROQ query as shown below. This is also explained in Knut's post that I linked to earlier in this post.

*[_type == "post"]{
  ...,
  body[]{
    ...,
    markDefs[]{
      ...,
      _type == "internalLink" => {
        "slug": @.reference->slug.current
      }
    }
  }
}

Now we can add this to our method as well.

if (type == "internalLink")
{
    var slug = markDef["slug"]?.ToString();
    start.Append($"<a href='posts/{slug}'>");
    end.Append("</a>");
}

And now we got our links rendering correctly!