Flexible page type restrictions in Optimizely CMS
A guide to creating more flexible content structures
In the Optimizely (formerly EPiServer) CMS the content structure is based on the restrictions that are set on every content type. You can use the AvailableContentTypes
in code or configure the "Available Page Types" in the admin interface for a page type to specify which page types are available when creating child content. The problem with this is approach is that the restrictions are the same for every page of the specific type, even though you may not want every page of that type to have the same restrictions. In this blog post I will describe how resolve this problem by making the content restrictions editable within pages in the CMS. There are however numerous other use cases that can be built using the same techniques.
Reusing content types across multiple sites is another good use case that requires increased flexibility. The child content of the reused types would normally have to be the same for every site, but in most cases you need the content structure to be different per site. Implementing site specific content restrictions would be very useful in this scenario. I will not go into detail on how to achieve this — however it will be very similar to the solution described below.
The content type selector
To make the page restrictions editable we will need a property that lists all the available page types. When creating a new child page, only the selected page types should then be displayed as options. The following selection factory will retrieve all page types.
public class ContentTypeSelectionFactory : ISelectionFactory
{
public IEnumerable<ISelectItem> GetSelections(ExtendedMetadata metadata)
{
var contentTypeRepo = ServiceLocator.Current.GetInstance<IContentTypeRepository>();
//Retrieve all page types
var contentTypes = contentTypeRepo.List()
.Where(ct => ct.Base == ContentTypeBase.Page)
.Select(x => x.CreateWritableClone() as ContentType)
.ToList();
var items = new List<ISelectItem>();
foreach (var contentType in contentTypes)
{
items.Add(new SelectItem { Text = contentType.Name, Value = contentType.ID });
}
return items;
}
}
Then to use the selection factory we can use the SelectMany
attribute on a property, which will allow the editor to select multiple options from the list. In this case I've added the property to the PageBase
class so that it is available for every page.
[Display(
Name = "Allowed Content Types",
GroupName = "Content Availability",
Order = 10)]
[SelectMany(SelectionFactoryType = typeof(ContentTypeSelectionFactory))]
public virtual string AllowedContentTypes { get; set; }
The page property will look like this.
Note that the group name is "Content Availability", which will add the property into a separate tab in the page. You can then set the group to only be editable by administrators. This way only admins can make changes to the content type availability.
The content type availability service
In the CMS there is a DefaultContentTypeAvailablilityService
that is used to filter the available content types by permissions when creating content such as pages and blocks. The ListAvailable
method of the service is called every time you create a piece new content. In our scenario we want to check the AllowedContentTypes
property of the parent page when creating a new child page — then filter the list so that only those page types are listed as options.
To do this we can create a custom implementation of the service that inherits the default service. Then we override the ListAvailable
method to filter the content types.
public class PropertyContentTypeAvailabilityService : DefaultContentTypeAvailablilityService
{
public PropertyContentTypeAvailabilityService(
ServiceAccessor<IContentTypeRepository> contentTypeRepositoryAccessor,
IAvailableModelSettingsRepository modelRepository,
IAvailableSettingsRepository typeSettingsRepository,
GroupDefinitionRepository groupDefinitionRepository,
IContentLoader contentLoader,
ISynchronizedObjectInstanceCache cache,
Lazy<ISettingsService<SiteSettingsPage>> settingsService)
: base(contentTypeRepositoryAccessor, modelRepository, typeSettingsRepository, groupDefinitionRepository,
contentLoader, cache)
{
}
public override IList<ContentType> ListAvailable(IContent content, bool contentFolder, IPrincipal user)
{
//Use the base service to filter by permissions and attributes
var contentTypes = base.ListAvailable(content, contentFolder, user);
return contentTypes.Where(ct => SelectByProperty(ct, content)).ToList();
}
private Func<ContentType, IContent, bool> SelectByProperty => (contentType, content) =>
{
if (contentType.ModelType == null) return true;
if (content is PageBase page && !page.AllowedContentTypes.IsNullOrEmpty())
{
return page.AllowedContentTypes.Split(',').Contains(contentType.ID.ToString());
}
return true;
};
}
For this to work we need to update the service context to use our custom service instead of the default implementation. We can do this in the dependency injection resolver as follows.
public void ConfigureContainer(ServiceConfigurationContext context)
{
context.ConfigurationComplete += (_, _) =>
{
context.Services.RemoveAll(typeof(ContentTypeAvailabilityService));
context.Services.AddSingleton<ContentTypeAvailabilityService, PropertyContentTypeAvailabilityService>();
};
}
Trying it out
On every page we can now update the available content types. As you can see here we only selected two page types. And as a result, when creating a new child page, only the selected two page types are displayed.