Auto-Register Block Previews in Umbraco
The Block Preview package by Rick Butterfield is a fantastic addition to any Umbraco project using Block List or Block Grid. It gives editors a real-time preview of their blocks right in the backoffice, instead of a generic placeholder.
But as your project grows and you add more blocks, you need to keep remembering to register each one in your BlockPreviewOptions. That’s easy to forget, and it means developers less comfortable with C# need to touch configuration code every time they create a new block.
What if registration just… happened automatically?
The Idea
The Block Preview package looks for views in specific default locations. If you follow those conventions, there’s no reason you should also have to manually tell the package which blocks to enable - it should be able to figure that out itself.
My approach:
- Scan the default view locations for
.cshtmlfiles - Also check for precompiled Razor views in the assembly
- Match file names to content type aliases
- Register only the blocks that have a corresponding view
This way, adding a new block with a preview is as simple as creating the view file. No C# changes required.
The Extension Method
Here’s an extension method that replaces the standard AddBlockPreview() registration:
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System.Reflection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Examine;
using Umbraco.Extensions;
using BlockPreview;
using BlockPreview.Settings;
public static class AddBlockPreviewsExtension
{
public static IUmbracoBuilder AddBlockPreviews(this IUmbracoBuilder builder)
{
var stylesheet = new string[] { "/build/css/main.css" };
builder.AddBlockPreview();
builder.Services.AddSingleton<IConfigureOptions<BlockPreviewOptions>>(sp =>
{
var env = sp.GetRequiredService<IWebHostEnvironment>();
var contentTypeService = sp.GetRequiredService<IContentTypeService>();
var contentTypeAliases = contentTypeService.GetAllContentTypeAliases().ToList();
return new ConfigureOptions<BlockPreviewOptions>(options =>
{
options.RichText = GetSettings(
env,
contentTypeAliases,
BlockPreviewConstants.DefaultViewLocations.RichText,
stylesheets);
options.BlockGrid = GetSettings(
env,
contentTypeAliases,
BlockPreviewConstants.DefaultViewLocations.BlockGrid,
stylesheets);
options.BlockList = GetSettings(
env,
contentTypeAliases,
BlockPreviewConstants.DefaultViewLocations.BlockList,
stylesheets);
});
});
return builder;
}
private static BlockWithStylesheetSettings GetSettings(
IWebHostEnvironment hostingEnvironment,
List<string> contentTypeAliases,
string defaultViewLocation,
List<string>? viewLocations = null,
string[]? stylesheets = null)
{
viewLocations = viewLocations.EmptyIfNull().ToList();
var contentTypes = GetViewNames(
hostingEnvironment,
contentTypeAliases,
viewLocations,
defaultViewLocation);
return new()
{
ViewLocations = viewLocations,
ContentTypes = contentTypes,
Enabled = contentTypes.Any(),
Stylesheets = stylesheets
};
}
private static List<string> GetViewNames(
IWebHostEnvironment hostingEnvironment,
List<string> contentTypeAliases,
List<string>? viewLocations,
string? defaultLocation = null)
{
var locations = (viewLocations ?? [])
.Append(defaultLocation)
.WhereNotNull()
.Select(CleanPath)
.Select(x => x.TrimEnd("{0}.cshtml"))
.ToList();
// Find view files on disk
var files = locations
.Select(hostingEnvironment.MapPathWebRoot)
.Where(Directory.Exists)
.SelectMany(location =>
Directory.EnumerateFiles(location, "*.cshtml", SearchOption.AllDirectories))
.Select(CleanPath)
.WhereNotNull();
// Find precompiled Razor views in the assembly
var assembly = Assembly.GetExecutingAssembly();
var allRazorViews = assembly
.GetCustomAttributes<RazorCompiledItemAttribute>()
.Select(x => x.Identifier)
.Select(CleanPath)
.WhereNotNull();
var razorViews = locations
.SelectMany(location => allRazorViews
.Where(view => view.InvariantStartsWith(location)));
// Match view names to content type aliases
return ((List<string>)[.. files, .. razorViews])
.Select(Path.GetFileNameWithoutExtension)
.Select(view => contentTypeAliases
.FirstOrDefault(alias => alias.InvariantEquals(view)))
.WhereNotNull()
.ToList();
}
private static string CleanPath(string path)
{
return path
.Replace('/', Path.DirectorySeparatorChar)
.Replace('\\', Path.DirectorySeparatorChar)
.EnsureStartsWith(Path.DirectorySeparatorChar);
}
}
How It Works
The extension method does three things:
-
Calls the standard registration -
builder.AddBlockPreview()still sets up the package as normal. -
Configures options via DI - By registering an
IConfigureOptions<BlockPreviewOptions>, we can modify the options after the default setup, with access to Umbraco services. -
Auto-discovers content types - For each editor type (Block Grid, Block List, Rich Text), it scans the default view locations and matches file names to content type aliases.
The GetViewNames method handles both scenarios:
- Views on disk in
wwwroot(development) - Precompiled Razor views baked into the assembly (production/deployment)
Using It
Replace your existing Block Preview registration in Program.cs or your composer:
// Before
builder.AddBlockPreview(options =>
{
options.BlockGrid.ContentTypes = ["heroBlock", "textBlock", "imageBlock"];
options.BlockGrid.Stylesheets = ["/build/css/main.css"];
});
// After
builder.AddBlockPreviews();
That’s it. Now any block with a view in the default location gets preview automatically.
Customization
You might want to customize the stylesheet path or add additional view locations. Simply modify the extension method:
public static IUmbracoBuilder AddBlockPreviews(
this IUmbracoBuilder builder,
string[] stylesheets = ["/css/main.css"])
{
// ... use the stylesheet parameter
}
Or if you have blocks that should explicitly not have preview (like a tracking code snippet block), you could add an exclusion list:
var excludedBlocks = new[] { "htmlSnippetBlock", "trackingCodeBlock" };
var contentTypes = GetViewNames(/* ... */)
.Where(x => !excludedBlocks.InvariantContains(x))
.ToList();
Benefits
- Zero maintenance - New blocks with views are automatically registered
- Developer-friendly - Frontend developers can add block views without touching C#
- Convention over configuration - Follow the default view locations and everything just works
- No forgotten registrations - If a view exists, it’s registered
Final Thoughts
This approach trades explicit configuration for convention-based discovery. For teams where multiple developers work on blocks, this removes a common friction point and source of “why isn’t my preview working?” questions.