Sitecore ASP.NET SDK and interactive components


Sitecore have had an SDK for ASP.Net (formerly known as .Net core) for JSS for a while, but unlike the Next.JS one it was not open source and was lacking features. Recently Sitecore announced a new focus on being more technology agnostic and the .Net SDK was relaunched now as Open Source and it is definitly being prioritized and Sitecore is working on closing the gap so all functionality should be available. It is not a completely new library, as it has existed for years, but it is gaining much more tracting now.

We would you use ASP.Net for the “head application” the rendering host of a new Sitecore based web application? Some of the benefits I see are:

  • Well-known technology stack (almost at least). The rendering host is based on ASP.Net and MVC a bit like traditional Sitecore CMS has been for years. It is not exactly the same as Microsoft and .Net has evolved significantly, but a lot still feels similar and the tooling etc.

  • Well-known behavior. We are still operation with full page loads, no partial updates etc. and hereby there are a bit less surprises such as missing tracking etc.

  • Keep your organization, less need for new competencies as you don’t need to give up and change all of the existing skills in your organization. It is more like an evolution than a revolution.

  • It is a faster runtime. Yes, .Net is faster than node. It is multi-threaded, it is more efficient in memory usage, it is more efficient on CPU and I/O1 2. It is also improving with each release and there is a continous focus on performance3. But we have seen so many posts about Next.JS being so fast? Yes, Next.JS do a lot of tricks to make a performing site even though it is based Node, static generation, partial updates, edge workings to mix together pregenerated parts etc. Recently Vercel even made compilation from node to Rust to get performant functions4. You can definitly get a performant site with Node.JS and Vercel a lot of additional steps are just added and you are probably need to buy this.

It is important to note that it is .Net (core) and not .Net Framework (4.8) as your old Sitecore MVC site. Hereby, it supports all operatingsystems, no need for Windows you can run it on MacOS or Linux. With the benefit of the headless architecture it is also not bound with all the actions that Sitecore MVC also are doing on a request.

But what about the frontend

Even though how much you might love the backend, there are today expectations on interactive parts of your web presence. While jQuery is still around there are benefits of the more modern frameworks such as componentization, data-binding, testing as the ecosystem of existing components and utilities you could tap into.

Islands architecture

As major parts of the site will be rendered server-side by the backend and only smaller parts will be enriched client-side, we found that the we actually wanted an Islands Archtecture.

The islands architecture encourages small, focused chunks of interactivity within server-rendered web pages. The output of islands is progressively enhanced HTML, with more specificity around how the enhancement occurs. Rather than a single application being in control of full-page rendering, there are multiple entry points. The script for these “islands” of interactivity can be delivered and hydrated independently, allowing the rest of the page to be just static HTML.5

We will render everything serverside and then each component or “island” will find and enrich the relevant DOM element(s) with the necesesary functionality.

As we are mostly focusing on enriching experiences, we actually find it simpler and even better that we in general don’t instatiate those components while editing in Pages (or Experience Editor).

Frameworks

In our recent project the developers already knew React so it was kind of obvious to look into this direction. However, we wanted to explore the options available to make the solution stunning and still balzing fast.

React have a lot of features and some are certain important for Single-page Applications but might not give as much value when we wanted to a server side rendered full page load and just some interactive parts on the pages. While features are nice it usually comes with a cost of complexity and size and while tree-shaking can help a lot, bundle size will matter for the website as it has to be downloaded and processed.

Preact is an alternative that is very similar and compatiable with React but its focus on size is significant. See below the output of b

Bundle analyzer output for empty react app bundle showing total of 554.5KB
Bundle analyzer output for empty preact app bundle showing total of 14.85KB

An empty app is 15 KB instead of 555 KB with react.

Preact it closer to native DOM (so you write class="something" instead of className="something"), use native events instead of React synthetic events etc. but overall we don’t see there are differences that will be a problem for us.

The ecosystem is important and a major benefit of React. Preact do support other React components by a small compatibility layer included in the core project, so it is not anything third-party. It will of course add a bit but still the difference is significant.

Below is a comparison where the react-multi-carousel package is included and hereby preact/compat is also loaded.

Bundle analyzer output for react app bundle with carousel showing total of 602.07KB
Bundle analyzer output for preact app bundle with acarousel showing total of 76.87KB

Still it is a matter of 77KB vs. 602KB

Implementation

A component in preact is very similar to react, eg. let’s create a component for adding an item to a basket.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
export const AddToBasketComponent = (props: AddToBasketProps) => {
    const { sku, name, price, textLabel, styles } = props;

    const handleClick = () => {
        addToBasket({
            sku, name, price,
            quantity: 1
        })
    }
    return (
        <button type="button" class={`add-to-basket btn ${styles}`} onClick={handleClick}>
            {textLabel} yeah
        </button>
    );
}

While in a traditional react application you would instantiate an <App> component on a almost top-level element, we will instead instantiate each component.

1
2
3
document.querySelectorAll(`[app="add-to-basket"]`).forEach(e => {
    render(<AddToBasketComponent ></AddToBasketComponent>, e);
});

Injecting properties

Most components will also need some model or configuration. We will have a server-side model with a razor view and we will transfer those (relevant) data to the client-side component.

We decided to render those properties as json (no surprise) and created a small HtmlHelper for this:

1
2
3
4
5
6
7
8
public static class ComponentRenderingExtensions
{
    public static HtmlString RenderClientSideProps(this BaseModel model)
    {
        var json = JsonSerializer.Serialize(model, model.GetType(), CreateOptions());
        return new HtmlString($"<script type=\"text/props\">{json}</script>");
    }
}

This will be rendered something like:

1
2
3
4
5
6
7
8
<script type="text/props">
{
    "textLabel": {
        "editable":"",
        "value":"Add to basket (sitecore text)"
    },
    "styles":"btn-light indent-top"
}</script>

However, while the editable and corresponding fields are important for editing, they are just making things more complicated for us. At least while we say that our islands are not important for editing.

We can achieve this with the

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static JsonSerializerOptions CreateOptions()
{
    var options = new JsonSerializerOptions
    {
        DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault,
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    };
    options.Converters.Add(new TextFieldJsonConverter());
    return options;
}

public class TextFieldJsonConverter : JsonConverter<TextField>
{
    public override TextField? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }

    public override void Write(Utf8JsonWriter writer, TextField value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.Value);
    }
}

So now our Razor view will look like

1
2
3
4
5
6
7
@using Sample.WebApp.StarterKit.Extensions
@model AddToBasket

<div app="add-to-basket">
	<div class="field-content" asp-for="@Model.TextLabel"></div>
	@Model.RenderClientSideProps()
</div>

And the output will be

1
2
3
4
5
6
7
8
9
<div app="add-to-basket">
  <div class="field-content">Add to basket (sitecore text)</div>
  <script type="text/props">
  {
    "textLabel":"Add to basket (sitecore text)",
    "styles":"btn-light indent-top"
  }
  </script>
</div>

When initialized we want to replace this simple server-side rendered component (the text label) with our interactive button.

Let’s have this as a reusable function

1
2
3
4
5
6
7
8
9
export function createIsland(appSelectorAttributeValue, Component) {
    document.querySelectorAll(`[app="${appSelectorAttributeValue}"]`)
    .forEach(e => {
        const json = e.querySelector('script[type="text/props"]')?.innerHTML;
        const props = json ? JSON.parse(json) : {};
        e.innerHTML = '';
        render(<Component {...props} ></Component>, e);
    });
}

so when initializing we will just call

1
createIsland('add-to-basket', AddToBasketComponent);

The downsides

As we don’t share model or implementation between backend and frontend we are not just rendering the same component server-side. We have two different implementations and we need to balance the effort on both sides to avoid making too much re-implementation.

As I see it, this is the major benefit of using Node runtime for server-side rendering. Here we are in charge of both ourselves, so far it is our expectation that it will help keep our bundles smaller. It might be that we will need to reuse the rendered html some day in the future (or it might be that the scope of this “island” is then wrong).

What’s next

The javascript and styling should of course be rendered on the page. On an upcoming post I will dig into, how we can have short and easy feedback while editing frontend and backend.