[DRAFT] This post is a draft and may change.

Adding Contextual Information to Block List Rendering in Umbraco

Draft
umbraco csharp blocklist

When rendering block list items in Umbraco, each block is rendered in isolation - it has no knowledge of its position in the list, what blocks come before or after it, or even how many blocks exist in total. This can be limiting when your design requires blocks to behave differently based on their context.

In this article, I’ll show you how to pass contextual information to your block list views, enabling smarter rendering decisions.

The Problem

Consider a common scenario: you have a “Headline” block that can appear multiple times on a page. For SEO purposes, the first headline should render as an <h1>, but subsequent headlines should use <h2>. Without context, each block view has no way of knowing if it’s the first of its kind.

Other scenarios where context matters:

  • Alternating layouts - Render odd/even items differently for visual variety
  • Connected sections - Remove bottom margin when the next block is a specific type
  • Numbered lists - Display sequential numbers without relying on CSS counters
  • First/last styling - Add special styling to the opening or closing block
  • Conditional spacing - Adjust padding based on adjacent block types

The block list partial view simply doesn’t have this information available by default.

The Solution

We can solve this by creating a BlockListContext record that captures all the contextual information a block might need, then pass it through ViewData when rendering each block.

Step 1: Create the BlockListContext Record

First, define a record that holds all the context we want to make available:

public record BlockListContext(
    IEnumerable<BlockListItem> List,
    BlockListItem? Current,
    int Index,
    bool IsFirst,
    bool IsLast,
    BlockListItem? Previous,
    BlockListItem? Next
);

This gives us:

  • List - The complete block list for advanced scenarios
  • Current - Reference to the current block
  • Index - Zero-based position in the list
  • IsFirst / IsLast - Quick boolean checks for boundary conditions
  • Previous / Next - Direct access to adjacent blocks

Step 2: Add Extension Methods

Next, create extension methods to build the context and work with ViewData:

public static class BlockListContextExtensions
{
    private const string BlockListContextKey = "BlockListContext";

    public static BlockListContext GetBlockListContext(
        this BlockListItem blockListItem,
        IEnumerable<BlockListItem> blockList)
    {
        var list = blockList.ToList();
        var index = list.IndexOf(blockListItem);
        var count = list.Count;

        return new BlockListContext(
            list,
            blockListItem,
            index,
            index == 0,
            index == count - 1,
            index > 0 ? list[index - 1] : null,
            index < count - 1 ? list[index + 1] : null
        );
    }

    public static BlockListContext? GetBlockListContext(
        this ViewDataDictionary viewData) =>
        viewData.TryGetValue(BlockListContextKey, out var value)
            ? value as BlockListContext
            : null;

    public static void SetBlockListContext(
        this ViewDataDictionary viewData,
        BlockListItem blockListItem,
        IEnumerable<BlockListItem> blockList) =>
        viewData[BlockListContextKey] = blockListItem.GetBlockListContext(blockList);

    public static void SetBlockListContext(
        this ViewDataDictionary viewData,
        BlockListContext blockListContext) =>
        viewData[BlockListContextKey] = blockListContext;
}

Step 3: Modify the Block List View

In your block list rendering view (or wherever you iterate over the blocks), set the context before rendering each block:

@foreach (var block in blocks)
{
    ViewData.SetBlockListContext(block, blocks);
    @await Html.PartialAsync(block.Content.ContentType.Alias, block)
}

Step 4: Use the Context in Block Views

Now in any individual block view, you can access the full context:

@using YourProject.Extensions
@model BlockListItem

@{
    var context = ViewData.GetBlockListContext();
}

@if (context?.IsFirst == true)
{
    <h1>@Model.Content.Value("headline")</h1>
}
else
{
    <h2>@Model.Content.Value("headline")</h2>
}

Practical Examples

Example 1: First Headline Gets h1

The classic SEO use case - ensure only one <h1> per page:

@{
    var context = ViewData.GetBlockListContext();
    var headlineTag = context?.IsFirst == true ? "h1" : "h2";
}

<@headlineTag class="headline">@Model.Content.Value("text")</@headlineTag>

When a “Text” block is followed by an “Image” block, remove the bottom margin to create a tighter visual connection:

@{
    var context = ViewData.GetBlockListContext();
    var nextIsImage = context?.Next?.Content.ContentType.Alias == "imageBlock";
    var marginClass = nextIsImage ? "mb-0" : "mb-8";
}

<div class="text-block @marginClass">
    @Model.Content.Value("richText")
</div>

Example 3: Alternating Background Colors

Create visual rhythm by alternating block backgrounds:

@{
    var context = ViewData.GetBlockListContext();
    var bgClass = context?.Index % 2 == 0 ? "bg-white" : "bg-gray-50";
}

<section class="@bgClass py-12">
    @* Block content *@
</section>

Example 4: Display Position Number

Show block position without CSS counters:

@{
    var context = ViewData.GetBlockListContext();
}

<div class="step">
    <span class="step-number">@((context?.Index ?? 0) + 1)</span>
    <div class="step-content">
        @Model.Content.Value("content")
    </div>
</div>

Example 5: Special First and Last Styling

Add hero styling to the first block and a CTA to the last:

@{
    var context = ViewData.GetBlockListContext();
}

<section class="@(context?.IsFirst == true ? "hero-section" : "standard-section")">
    @* Content *@
    
    @if (context?.IsLast == true)
    {
        <div class="cta-banner">Ready to get started?</div>
    }
</section>

Supporting Block Grid

The same pattern works for Block Grid. You can add overloaded extension methods to support both:

public static BlockListContext GetBlockListContext(
    this IBlockReference<IPublishedElement, IPublishedElement> blockReference,
    BlockListModel blockList) =>
    // Implementation for BlockListModel
    
public static BlockListContext GetBlockListContext(
    this IBlockReference<IPublishedElement, IPublishedElement> blockReference,
    BlockGridModel blockGrid) =>
    // Implementation for BlockGridModel

Final Thoughts

Passing contextual information to block list items opens up many possibilities for smarter, more dynamic rendering. Instead of relying on CSS tricks or JavaScript manipulation, your blocks can make informed decisions at render time based on their position and neighbors.

The pattern is straightforward: capture context when iterating, pass it through ViewData, and retrieve it in your block views. This keeps your code clean, your blocks reusable, and your rendering logic where it belongs - on the server.