Theme:

Importing Block List Items Programmatically in Umbraco 17

A practical, problem-solving guide on how to import Block List items programmatically in Umbraco 17 instead of manually creating them in the backoffice.

When working with Umbraco, Block List editors are one of the most flexible ways to structure repeating content. They’re powerful, clean, and editor-friendly, but can be a bit tricky to populate programmatically.

Recently, I faced a similar challenge: I had a timeline component which utilised the Block List editor to populate the various events on the timeline as blocks.

Manually adding each block through the Umbraco backoffice was theoretically possible… but realistically, it was going to be slow and repetitive.

So instead of entering every item manually, I built a custom import API endpoint to generate all the Block List items automatically.

Let’s dive in and find out how to solve this problem.

Quick Project Setup

Before jumping into Block List imports, let’s first set up a small test project to experiment.

For this walkthrough, we’ll spin up an Umbraco 17 RC2 site and install the Clean starter kit, as it gives us a few pages and blocks to work with so we’re not starting from a blank install.

The setup commands were generated using this Package Script Writer link.

Shout-out to Paul Seal for creating both the Package Script Writer and the Clean starter kit. These tools make spinning up Umbraco sites incredibly easy.

You can also run them manually as shown below:


# Ensure we have the version specific Umbraco templates
dotnet new install Umbraco.Templates::17.0.0-rc2 --force

# Create solution/project
dotnet new sln --name "UmbraTest"
dotnet new umbraco --force -n "UmbraTest"  --friendly-name "Administrator" --email "admin@example.com" --password "1234567890" --development-database-type SQLite
dotnet sln add "UmbraTest"


#Add starter kit
dotnet add "UmbraTest" package clean

#Add Packages
#Ignored Clean as it was added as a starter kit

dotnet run --project "UmbraTest"

Script to spin up a test Umbraco 17 website.

Creating the Content Structure

With the project running, the next step is to set up the content structure we’ll be importing into.

In the backoffice:

  1. Create a new Element Type called Timeline Item, at the path Document Types > Elements > Content Models > Timeline with the following fields:

    • Event Name — Textstring

    • Period — Textstring (this will hold something like "1990–1995")

    The Timeline Item element type

  2. Next, head over to Data Types and create a new one:

    • Name: [BlockList] Timeline
    • Property Editor: Block List
    • Available Blocks: Pick the Timeline Item element.
    • (Optional) Set the Label of the Timeline Item to ${ eventName }, ${period }.

    The [BlockList] Timeline datatype

    Setting the label of the Timeline Item

  3. Now assign this Block List to a page.

    • Go to Document Types > Pages.
    • Open the Content document type.
    • Add a new property called Timeline Items.
    • Set its Property Editor to the [BlockList] Timeline we created in the previous step.

    Adding the Property to the Content document type

Understanding Block List values

Before looking at the code, it’s important to understand how Block List values are structured. A Block List stores three pieces of data:

layout

It defines the order of blocks and ties each item to a UDI. This is basically the index of blocks.


{
  "Umbraco.BlockList": [
    { "contentUdi": "umb://element/123..." },
    { "contentUdi": "umb://element/456..." }
  ]
}

An example of the layout data

This tells Umbraco:

  • which blocks exist
  • which order they appear in the editor
  • which UDI belongs to each block

contentData

This holds the actual content inside each block. Every entry must correspond to a UDI used in layout.

contentTypeKey must match the GUID of the element type (in our case, timelineItem).


[
  {
    "udi": "umb://element/123...",
    "contentTypeKey": "GUID of timelineItem",
    "eventName": "Fr Peter",
    "period": "2020 - 2025"
  }
]

An example of the contentData

settingsData

This is for the optional settings element type.

Even if you're not using settings, Umbraco expects this key to exist, because the Block List format includes:

  • content block
  • settings block

Building the Timeline Import API

Now that we understand how Block List values are structured (layout, contentData, settingsData), we can start putting the import logic together.

To handle the import, we’ll create a small API endpoint that accepts a JSON payload and writes the data into the Timeline Items Block List property on the selected page.

The Request Body

To keep things organised, the API uses two simple models to represent the incoming JSON.

Create a class ImportModel.cs within the Umbraco solution.


public class ImportModel
{
    public string PageGuid { get; set; }
    public string BlockListAlias { get; set; }
    public List<TimelineImportModel> Timelines { get; set; } = new List<TimelineImportModel>();
}

public class TimelineImportModel
{
    public string Name { get; set; }
    public string StartDate { get; set; }
    public string EndDate { get; set; }
}

The Controller

Inside your Umbraco project, add a Controllers folder if it doesn’t already exist, create a new file named TimelineImportController.cs.

This controller accepts a JSON payload of timeline items, converts them into the required Block List JSON structure, and then saves and publishes the updated page.

Here’s the full controller code:


[ApiController]
[Route("/umbraco/api/timelineimport")]
public class TimelineImportController : Controller
{
    readonly IContentService _contentService;
    readonly IContentTypeService _contentTypeService;

    public TimelineImportController(
        IContentService contentService,
        IContentTypeService contentTypeService
        )
    {
        _contentService = contentService;
        _contentTypeService = contentTypeService;
    }

    [HttpPost("import")]
    public IActionResult Import([FromBody] ImportModel importModel)
    {
        try
        {
            if (!Guid.TryParse(importModel.PageGuid, out var pageId))
                return BadRequest("Invalid pageGuid value.");

            // find the page that contains the Block List we want to update
            var page = _contentService.GetById(pageId);

            if (page == null)
                return NotFound("Page not found.");

            // ensure the Block List property actually exists on this Document type
            if (!page.Properties.Contains(importModel.BlockListAlias))
                return BadRequest($"Property '{importModel.BlockListAlias}' does not exist.");

            // get the element type used for each timeline block
            var timelineItemType = _contentTypeService.Get("timelineItem");

            if (timelineItemType == null)
                return NotFound("timelineItem Element Type not found.");

            // These three lists form the core of a Block List value
            var layout = new List<Dictionary<string, string>>();
            var contentData = new List<Dictionary<string, string>>();
            var settingsData = new List<Dictionary<string, string>>();

            foreach (var item in importModel.Timelines)
            {
                var contentUdi = Udi.Create("element", Guid.NewGuid()).ToString();
                var settingsUdi = Udi.Create("element", Guid.NewGuid()).ToString();

                // Layout entry describes the block's position + content reference
                layout.Add(new Dictionary<string, string>
                {
                    { "contentUdi", contentUdi },
                    //{ "settingsUdi", settingsUdi } // Settings UDI can be added here if your blocks use settings
                });

                // content dictionary will store the properties defined on the element type
                var contentDict = new Dictionary<string, string>
                {
                    { "contentTypeKey", timelineItemType.Key.ToString() },
                    { "udi", contentUdi },
                    { "eventName", item.Name },
                    { "period", $"{item.StartDate?.ToString() ?? ""} - {item.EndDate?.ToString() ?? ""}" }
                    // add more block-level properties as needed
                };

                contentData.Add(contentDict);

                // settings data (if any)
                //var settingsDict = new Dictionary<string, string>
                //{
                //    { "contentTypeKey", timelineItemType.ToString() },
                //    { "udi", settingsUdi },
                //    // settings props
                //    { "settingAlias1", item.Setting1 },
                //};

                //settingsData.Add(settingsDict);
            }

            // build final Block List JSON structure
            var blockListObject = new
            {
                layout = new Dictionary<string, object>
                {
                    { "Umbraco.BlockList", layout }
                },
                contentData,
                settingsData
            };

            var json = JsonConvert.SerializeObject(blockListObject);

            // save the Block List JSON back to the content item
            page.SetValue(importModel.BlockListAlias, json);
            _contentService.Save(page);

            _contentService.Publish(page, ["*"]);

            return Ok("Timeline items imported successfully.");
        }
        catch (Exception ex)
        {
            return StatusCode(500, $"Import failed: {ex.Message}");
        }
    }
}

Testing the Timeline Import API

With the controller and models in place, we can now test the import endpoint.

Endpoint URL

Rebuild the project and get the site running up again.

If you're running the site locally, the endpoint will look like:

POST https://localhost:44344/umbraco/api/timelineimport/import

(Your port may differ based on your setup.)

Sample Payload

The import API expects a JSON body matching the ImportModel and TimelineImportModel structure. Here’s a simple example you can use for testing:


{
  "pageGuid": "PUT-YOUR-PAGE-GUID-HERE",
  "blockListAlias": "timelineItems",
  "timelines": [
    { "name": "Fr Peter", "startDate": "1996", "endDate": "1999" },
    { "name": "Fr John", "startDate": "1990", "endDate": "1995" }
  ]
}

I also have a sample payload gist which you can use to import a lot more items.

Steps to Test

  1. Open Postman or Yaak or any REST Client of your choice.
  2. Create a POST request to:

    https://localhost:44344/umbraco/api/timelineimport/import

  3. Set the Headers:

    Content-Type: application/json

  4. Paste the sample JSON body.

  5. Replace "pageGuid" with the actual GUID of your Content node. (You can copy it from the Umbraco backoffice > Info tab.)
  6. Hit Send.

    Make the Import API Request from the Yaak REST Client

What to Expect

  • If everything is correct, the response should be:

    Timeline items imported successfully.

  • Go to the page in the Umbraco backoffice.

  • Open the Timeline Items property.
  • You should now see each imported entry appear as a block, populated with:

    • Event Name
    • Period

    The imported timeline item blocks on the Home node.

    An imported Timeline Item block

Bonus: Doing the Same in Umbraco 13?

If you're on Umbraco 13 and want to build Block List items programmatically, I’ve added a small code sample in this GitHub Gist: Umbraco 13 Block List Import Example

Wrapping Up

That's it! No manual clicking. No creating blocks one by one. The importer does it all for you.

Hopefully this little walkthrough gives you some ideas of how you can use the same approach in your own projects. If you spot ways it could be improved, or you’ve solved the same problem differently, I’d honestly love to hear about it.

You can view the full project at https://github.com/manutdkid77/umbraco-blocklist-import-24days.

If you give this a try and run into any issues, feel free to reach out to me. Always happy to help.

Wishing you a wonderful Christmas in advance! 🎄✨