Valdis Iljuconoks previously helped me understand how to effectively implement AllowedTypes restrictions with interfaces, something like [AllowedTypes(typeof(INestedContent))]
– which is a beautiful solution for building a block library. This makes our blocks and their Content Areas only concern themselves with specific interfaces. In our case, we usually have layers such as IPageContent
(for stripes, grid structures, etc), INestedContent
(for heroes, teasers, forms, maps, videos, and the like), and ICallToAction
(for buttons, image buttons, etc). This is incredibly useful if you don’t want all of your blocks to know about all of your other blocks, or if your blocks live across independent feature projects.
This solution, however, starts to show some weaknesses when you move to a multisite implementation with a shared block layer. In a lot of cases, we will ultimately build blocks that are specific to a given website. In a SCORE solution, for instance, these blocks would implement IPageContent
or INestedContent
interface so that they plug-and-play nicely with the common block layer. The problem arises, however, when you want to restrict blocks to SiteA and not allow them to be created on SiteB and SiteC.
To set this scenario up, Valdis again has a very nice blog series on MVC Area support in Episerver. If this concept is new to you, I highly recommend you read up here and here.
To extend this set up, what I needed was the ability to decorate my globally shared blocks with a new attribute: [RestrictTo(new [] { "SiteA", "SiteX" })]
. If the editor is on a page for SiteA or SiteX, then they should be able to insert a block of this type. If the editor is on SiteB or SiteC, then this block should not be an available option. From everything I can tell, the only out-of-the-box mechanism to support this in Episerver is to restrict blocks by user role. This wouldn’t work effectively in my case (and in a lot of cases), because our user roles don’t always correspond 1:1 with websites. So how do we do it?
First, let’s define the attribute in question. It’s quite simple:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public class RestrictToAttribute : Attribute { public string[] Sites { get; set; } public RestrictToAttribute(string[] sites) { Sites = sites; } }
Secondly, to use this attribute, we should do something like this for our blocks (this also works for PageData if you need it):
[ContentType(...)] [RestrictTo(new[] { "SiteA" })] // move this to const somewhere in your solution public class SiteSpecificBlock : BlockData, INestedContentBlock { // some specific impl here }
From a consumer standpoint and as a developer entering into a project- this looks like a very friendly way to achieve site based restrictions. This is really easy to follow and easy to leverage. To actually make it work, what we need to do is shim in our own implementation of the EPiServer.DataAbstraction.Internal.DefaultContentTypeAvailablilityService
. Unfortunately this is part of Episerver’s internal API, so if you know of a better way to handle this, please leave a comment down below!
To shim in our own implementation, add this to your container configuration:
services.RemoveAll(typeof(ContentTypeAvailabilityService)); services.AddTransient<ContentTypeAvailabilityService, RestrictedContentTypeAvailabilityService>();
This will ensure that anyone who asks for ContentTypeAvailabilityService from the container will instead receive the RestrictedContentTypeAvailabilityService (even the Episerver runtime as it’s drawing the editing interface). From here, we just need to manipulate the list of types that Episerver’s service is returning back out, taking into consideration the RestrictTo
attribute. We’ll inherit Epi’s service and call their base method, with some special filtering layered overtop:
public class RestrictedContentTypeAvailabilityService : DefaultContentTypeAvailablilityService { private readonly IContentLoader _contentLoader; private readonly ISiteDefinitionResolver _siteDefinitionResolver; public RestrictedContentTypeAvailabilityService( ServiceAccessor<IContentTypeRepository> contentTypeRepositoryAccessor, IAvailableModelSettingsRepository modelRepository, IAvailableSettingsRepository typeSettingsRepository, GroupDefinitionRepository groupDefinitionRepository, IContentLoader contentLoader, ISynchronizedObjectInstanceCache cache, ISiteDefinitionResolver siteDefinitionResolver) : base(contentTypeRepositoryAccessor, modelRepository, typeSettingsRepository, groupDefinitionRepository, contentLoader, cache) { if (contentLoader == null) throw new ArgumentNullException("contentLoader"); if (siteDefinitionResolver == null) throw new ArgumentNullException("siteDefinitionResolver"); _contentLoader = contentLoader; _siteDefinitionResolver = siteDefinitionResolver; } // this method is called everytime "Add a new block" or "Add a new page" is called- it fetches the types available public override IList<ContentType> ListAvailable(IContent content, bool contentFolder, IPrincipal user) { var baseList = base.ListAvailable(content, contentFolder, user); return Filter(baseList, content).ToList(); } // to filter, simply look at each model type being returned and inspect if it has the RestrictTo attribute // if it does have the attribute, ensure that the SiteDefinition.Current is contained within the list of // allowed websites. If it is, allow the model to be returned, otherwise do not protected virtual IEnumerable<ContentType> Filter(IList<ContentType> contentTypes, IContent content) { var siteDefinition = content != null ? _siteDefinitionResolver.GetByContent(content.ContentLink, false, false) : SiteDefinition.Current; foreach (var targetType in contentTypes) { if (siteDefinition == null) yield return targetType; var modelType = targetType.ModelType; if (modelType != null) { // attempt to fetch an instance of RestrictTo from the model var attributeVal = (RestrictToAttribute) Attribute.GetCustomAttribute(modelType, typeof(RestrictToAttribute)); if (attributeVal != null) { var currentSite = siteDefinition.Name; // compare current site context name against the list of sites in the attribute if (attributeVal.Sites.Any(x => x.Equals(currentSite, StringComparison.InvariantCultureIgnoreCase))) { yield return targetType; } } else { yield return targetType; } } else { yield return targetType; } } } }
Again, please let me know if there is a better way to handle this- but for now, this is the best solution I could find to the problem. Enjoy!
]]>Originally posted in 2018, I’ve decided to update this post as the script has evolved a bit over the years. If you are unfamiliar with Powershell in the Sitecore context, then I highly recommend you check out Sitecore Powershell Extensions. This script, in particular, we have used on multiple projects – and it enables the Content Authoring team to quickly copy languages across multiple content items in bulk. Every new project I start up, the team asks me to include this feature.
From anywhere within the Content Tree, I want the ability to right click an item and choose to copy its language. I also want the ability to copy datasources and subitems. Here’s how you should access it, and what the modal will look like:
To create this functionality, I have added a context menu script to my tenant’s Powershell script library. This could also be a Feature in your Helix setup. Mine looks like this:
First of all, we need a function that lets us pull the datasources of an item. I’m going to implement it so that it skips Above Page Content and Below Page Content placeholders, as I don’t want to run this copying command on my header and footer snippets. You may want to change this depending on your assembly practices.
function GetItemDatasources { [CmdletBinding()] param([Item]$Item) # grab all datasources that are not header and footer elements return Get-Rendering -Item $item -FinalLayout -Device (Get-LayoutDevice -Default) | Where-Object { -not [string]::IsNullOrEmpty($_.Datasource) } | Where-Object { $_.Placeholder -ne 'Above Page Content' } | Where-Object { $_.Placeholder -ne 'Below Page Content' } | ForEach-Object { Get-Item "$($item.Database):" -ID $_.Datasource } }
Next, I need the main entry point into the program. Since this is a context menu script, the user invokes it by right-clicking an item within the tree. The get-location cmdlet will return the item that was clicked on.
$location = get-location
Now, we need to build up some options to display to the user. I want them to pick languages, a copy mode, and some additional options. That’s what all of this logic does.
$languages = Get-ChildItem "master:\sitecore\system\Languages"; $currentLanguage = [Sitecore.Context]::Language.Name; $langOptions = @{}; foreach ($lang in $languages) { $langOptions[$lang.Name] = $lang.Name; } $ifExists = @{}; $ifExists["Append"] = "Append"; $ifExists["Skip"] = "Skip"; $ifExists["Overwrite Latest"] = "OverwriteLatest";
Once I’ve got my arguments ready, I can go ahead and prompt the user with a dialog. Within this dialog, I’ve added some columns, help text, etc. At the very end, I want to make sure that they hit the OK button. You can see that if the $result is not set to “ok”, then we exit the script.
$result = Read-Variable -Parameters ` @{ Name = "originLanguage"; Value=$currentLanguage; Title="Origin Language"; Options=$langOptions; }, @{ Name = "destinationLanguages"; Title="Destination Language(s)"; Options=$destinationOptions; Editor="checklist"; }, @{ Name = "includeSubitems"; Value=$false; Title="Include Subitems"; Columns = 4;}, @{ Name = "includeDatasources"; Value=$false; Title="Include Datasources"; Columns = 4 }, @{ Name = "includeSnippets"; Value=$false; Title="Include Snippet Datasources"; Columns = 4 }, @{ Name = "ifExists"; Value="Skip"; Title="If Exists"; Options=$ifExists; Tooltip="Append: Create new language version with copied content.<br>Skip: do nothing if destination has language version.<br>Overwrite Latest: overwrite latest language version with copied content."; } ` -Description "Select an origin and destination language, with options on how to perform the copy" ` -Title "Copy Language" -Width 650 -Height 660 -OkButtonName "Proceed" -CancelButtonName "Cancel" -ShowHints if($result -ne "ok") { Exit }
Now, we need to calculate which items the user selected based upon the parameters they’ve chosen (include subitems, include datasources, include snippet datasources, etc). We’ll store the list of items in an object called $items, and we’ll remove duplicates at the end.
$items = @() $items += Get-Item $location # add optional subitems if ($includeSubitems) { $items += Get-ChildItem $location -Recurse } # add optional datasources if ($includeDatasources) { Foreach($item in $items) { $items += GetItemDatasources($item) } } # add optional datasource subitems if ($includeSnippets) { $items += $items | Where-Object { $_.TemplateName -eq 'MySite Snippet' } | ForEach-Object { GetItemDatasources($_) } } # Remove any duplicates, based on ID $items = $items | Sort-Object -Property 'ID' -Unique
At this point, I want the user to confirm that I’ve pulled the necessary items. They can’t really see a list of items, but they should have an idea of roughly how many they’re about to translate. For instance, if they think they’re translating 5 items, but the list comes back with 1200 items, then this is a chance for them to cancel the execution and try again.
$message = "You are about to update <span style='font-weight: bold'>$($items.Count) item(s)</span> with the following options:<br>" $message += "<br><table>" $message += "<tr><td style='width: auto'>Origin Language:</td><td>$originLanguage</td></tr>" $message += "<tr><td style='width: auto'>Destination Languages:</td><td>$destinationLanguages</td></tr>" $message += "<tr><td style='width: auto'>Include Subitems:</td><td>$includeSubitems</td></tr>" $message += "<tr><td style='width: auto'>Include Datasources:</td><td>$includeDatasources</td></tr>" $message += "<tr><td style='width: auto'>Include Snippet Datasources:</td><td>$includeSnippets</td></tr>" $message += "<tr><td style='width: auto'>Copy Method:</td><td>$ifExists</td></tr>" $message += "</table>" $message += "<br><p style='font-weight: bold'>Are you sure?</p>" $proceed = Show-Confirm -Title $message if ($proceed -ne 'yes') { Write-Host "Canceling" Exit }
At the end, the algorithm is pretty simple. All we need to do is take each item and run it through the Add-ItemLanguage command, passing in the different options that the user elected.
$items | ForEach-Object { Add-ItemLanguage $_ -Language $originLanguage -TargetLanguage $destinationLanguages -IfExist $ifExists }
Here’s the full script I ended up with, in all of its glory.
function GetItemDatasources { [CmdletBinding()] param([Item]$Item) # grab all datasources that are not header and footer elements return Get-Rendering -Item $item -FinalLayout -Device (Get-LayoutDevice -Default) | Where-Object { -not [string]::IsNullOrEmpty($_.Datasource)} | Where-Object { $_.Placeholder -ne 'Above Page Content' } | Where-Object { $_.Placeholder -ne 'Below Page Content' } | ForEach-Object { Get-Item "$($item.Database):" -ID $_.Datasource } # ForEach-Object { Write-Host ($_ | Format-List | Out-String) } } $location = get-location $user = Get-User -Current $languages = Get-ChildItem "master:\sitecore\system\Languages" $currentLanguage = [Sitecore.Context]::Language.Name $langOptions = @{}; $destinationOptions = @{}; foreach ($lang in $languages) { $langOptions[$lang.Name] = $lang.Name if (Test-ItemAcl -Identity $user -Path $lang.Paths.Path -AccessRight language:write) { $destinationOptions[$lang.Name] = $lang.Name } } $ifExists = @{}; $ifExists["Append"] = "Append"; $ifExists["Skip"] = "Skip"; $ifExists["Overwrite Latest"] = "OverwriteLatest"; $result = Read-Variable -Parameters ` @{ Name = "originLanguage"; Value=$currentLanguage; Title="Origin Language"; Options=$langOptions; }, @{ Name = "destinationLanguages"; Title="Destination Language(s)"; Options=$destinationOptions; Editor="checklist"; }, @{ Name = "includeSubitems"; Value=$false; Title="Include Subitems"; Columns = 4;}, @{ Name = "includeDatasources"; Value=$false; Title="Include Datasources"; Columns = 4 }, @{ Name = "includeSnippets"; Value=$false; Title="Include Snippet Datasources"; Columns = 4 }, @{ Name = "ifExists"; Value="Skip"; Title="If Exists"; Options=$ifExists; Tooltip="Append: Create new language version with copied content.<br>Skip: do nothing if destination has language version.<br>Overwrite Latest: overwrite latest language version with copied content."; } ` -Description "Select an origin and destination language, with options on how to perform the copy" ` -Title "Copy Language" -Width 650 -Height 660 -OkButtonName "Proceed" -CancelButtonName "Cancel" -ShowHints if($result -ne "ok") { Exit } Write-Host "originLanguage = $originLanguage" Write-Host "destinationLanguages = $destinationLanguages" $items = @() $items += Get-Item $location # add optional subitems if ($includeSubitems) { $items += Get-ChildItem $location -Recurse } # add optional datasources if ($includeDatasources) { Foreach($item in $items) { $items += GetItemDatasources($item) } } # add optional datasource subitems if ($includeSnippets) { $items += $items | Where-Object { $_.TemplateName -eq 'MySite Snippet' } | ForEach-Object { GetItemDatasources($_) } } # Remove any duplicates, based on ID $items = $items | Sort-Object -Property 'ID' -Unique $items | ForEach-Object { Write-Host ($_.ItemPath | Sort-Object | Format-List | Out-String) } $message = "You are about to update <span style='font-weight: bold'>$($items.Count) item(s)</span> with the following options:<br>" $message += "<br><table>" $message += "<tr><td style='width: auto'>Origin Language:</td><td>$originLanguage</td></tr>" $message += "<tr><td style='width: auto'>Destination Languages:</td><td>$destinationLanguages</td></tr>" $message += "<tr><td style='width: auto'>Include Subitems:</td><td>$includeSubitems</td></tr>" $message += "<tr><td style='width: auto'>Include Datasources:</td><td>$includeDatasources</td></tr>" $message += "<tr><td style='width: auto'>Include Snippet Datasources:</td><td>$includeSnippets</td></tr>" $message += "<tr><td style='width: auto'>Copy Method:</td><td>$ifExists</td></tr>" $message += "</table>" $message += "<br><p style='font-weight: bold'>Are you sure?</p>" $proceed = Show-Confirm -Title $message if ($proceed -ne 'yes') { Write-Host "Canceling" Exit } Write-Host "Proceeding with execution" $items | ForEach-Object { Add-ItemLanguage $_ -Language $originLanguage -TargetLanguage $destinationLanguages -IfExist $ifExists }
Always tests your scripts before putting them into production. This also assumes you have a feature similar to Snippets from SCORE or SXA. You can easily remove that portion if you need to. I hope this helps you in your Sitecore journey…
]]>Episerver Find is a great and easy way to integrate search into your Episerver powered website. One often overlooked aspect, however, is keeping your search indexes lean. By default, Episerver will index all instances of content across your website, whether it’s Pages, Blocks, Media, or Folders- Find doesn’t care and will index all of it. If you follow atomic design principles or have quite a large volume of content, you may find that you hit the limits of your index prematurely. Rather than increasing your subscription blindly, you should first look at if all of the content within your index is needed.
To view some basic diagnostics about your index, login to Episerver and navigate to the Find tool. From the Overview tab, you should see a list of all content types that you have indexed, as well as how many items are within the index. For instance, it may look like this:
As you can see, we have over 91,000 items in the index, with 77,000 of those items being of type EPiServer.Core.ContentAssetFolder
.
In most cases, search with Find usually revolves around searching for pages. Rarely do we need to leverage Find to pluck out any other elements such as Blocks or Folders (although it can be useful at the API level sometimes). Because of this, we can completely exclude these from our index. Not only will this squeeze a little more performance out of your index (although likely negligible), but it may also drop you down a pricing tier in terms of the volume of content you require. To remove items from your index, add this to your Find configuration:
[ModuleDependency (typeof (FindInitializationModule), typeof (IndexingModule))] public class FindInitialization : IInitializableModule { private static bool _initialized; public void Initialize (InitializationEngine context) { if (_initialized) return; // Remove specific items by type. In our case, we don't want images or our base type for // all blocks to be indexed. Extend with additional types as necessary ContentIndexer.Instance.Conventions.ForInstancesOf<ImageData>().ShouldIndex (p => false); ContentIndexer.Instance.Conventions.ForInstancesOf<ScoreSiteBlockData>().ShouldIndex (p => false); // include any additional logic related to computed fields, etc. _initialized = true; } public void Uninitialize (InitializationEngine context) { } }
For context, on a recent project we found that our index dropped an entire order of magnitude down in quantity. Hope this helps!
]]>When building highly performant web applications, it’s always important to consider your caching strategy. There are some generic things you can do to make your overall website faster (such as setting efficient client-side cache policies), but often times a much overlooked performance pitfall involves making too many API calls. Luckily, Episerver’s Object Caching can help you avoid this trap.
Imagine a scenario where you need to look up data from a back-office system (products, prices, locations, reviews… whatever). The data is exposed as a REST endpoint, and accepts a number of query parameters to retrieve various slices of data. This is quite common in web architecture, and is one of the pinnacles of a modern and rich web experience. Often times we integrate our systems together with “service layer glue” to avoid monolithic architectures, but if we do this wrong we can introduce massive performance impacts.
Let’s take the reviews example. Suppose I have a product details page, and reviews are displayed at the bottom of this page below-the-fold. Every time the product detail page loads, reviews need to be fetched and loaded as well. These reviews could be ajax’d in or loaded on initial page load- the outcome is still the same: the page isn’t fully loaded until the reviews have loaded.
For instance, you may have a simple service like so:
interface IReviewService { List<Review> FetchReviewsFor(string productId); }
Now, the implementation of this service probably involves an expensive operation. It may be using HttpClient or RestSharp to fetch reviews from an API, they could be fetched from a database, etc. The actual implementation doesn’t matter, so I’ve mocked it up for demonstration purposes:
List<Review> FetchReviewsFor(string productId) { List<Review> retVal = new List<Review>(); retVal = ...; // logic here to read from external system return retVal; }
The problem is that this operation may take some time to execute, and this execution may be blocking the calling thread. Sure, we could implement this as async, but that doesn’t affect your actual page speed if this is called at page rendering time. On top of this, our implementation is not very fault tolerant, as outages in the underlying review storage system will trigger outages for anyone calling this method. This is where a caching layer really becomes beneficial.
Imagine if we could write the above code like this:
List<Review> FetchReviewsFor(string productId) { var cacheKey = $"reviews-for-{productId}"; List<Review> retVal = _cacheService.Get<List<Review>>(cacheKey); if (retVal == null) { retVal = ...; // logic here to read from external system _cacheService.Insert(cacheKey, retVal); } return retVal; }
In this updated logic, we first attempt to fetch a list of review objects from our in-memory cache. If we find them, then we return them from the cache. If we don’t find them, then we fetch them from the source and cache the results for the next caller. There’s an implicit assumption here that we don’t need review data to be always up to date, and that eventually the cache will “clear itself out”. So how do we get ahold of this special _cacheService
object?
To achieve this, we can create a facade around Epi’s native IObjectInstanceCache
and inject that wrapper as a dependency into our services. A good starting point implementation looks like this:
public class CacheService : ICacheService { private IObjectInstanceCache _cache; private const string MASTER_KEY = "mysite-master-key"; public CacheManager(IObjectInstanceCache cache) { _cache = cache; } public T Get<T>(string key) where T : class { return _cache.Get<T>(key, ReadStrategy.Immediate); } public CacheEvictionPolicy GetDefaultPolicy() { int minutes = 5; return new CacheEvictionPolicy(new TimeSpan(0, minutes, 0), CacheTimeoutType.Absolute, Enumerable.Empty<string>(), new List<string>() { MASTER_KEY }); } public void Insert(string key, object value) { var cachePolicy = GetDefaultPolicy(); Insert(key, value, cachePolicy); } public void Clear(string key) { _cache.Remove(key); } public void ClearAll() { _cache.Remove(MASTER_KEY); } }
The important thing to note here, is that GetDefaultPolicy()
is currently hard-coded to cache results for up to 5 minutes. This is a setting that you could expose within the CMS or push to a configuration file. You may want this to be aggressively small (such as 5 minutes or less), or ramped up to be very conservative, such as 2 or more hours. The other thing to note is that I am tying this caching policy to all insertions and all cached objects are tied to the same master key. Feel free to swap this out as you see fit. You may also invoke the Clear()
or ClearAll()
operations upon specific actions, such as a content publish, or as a manual tool for site administrators when debugging issues.
This is a common strategy that I have used on every project in the past few years, regardless of whether the platform is Episerver or otherwise. I find this to be highly effective, and putting the right strategy in place really does set you up for being that much faster when it comes to implementing modern web solutions.
Enjoy!
]]>
While developing new features in Episerver CMS, it can be incredibly useful to have a local copy of the production content. Here are the steps required for extracting content from Episerver’s DXP platform and restoring it into your local environment for development.
The first step in the process is the only one you can’t fully control. You need to initiate a database export from the PaaS portal. Navigate to paasportal.episerver.net, select the appropriate account, and initiate a production export from the Troubleshoot tab.
This process takes anywhere from a few minutes to a few days. I personally like to initiate this after a successful production deployment, or after we do large content manipulation efforts. Once it’s complete, you’ll see a link to download a .bacpac
file in this same section.
Once you receive the .bacpac
file, you’ll need to import it into your local development environment. I always run Episerver under IIS with SQL Server, so these instructions are tailored to that setup (as opposed to IIS Express and SQL Express). I usually start by turning off IIS, closing existing connections to my current database, and deleting it. From there, run a Data Tier import to restore the production .bacpac
with the same name as your local dev setup.
Another tip here: All developers working on the same solution share a SQL Alias called “episerver” resolving to their local SQL installation. We all use a local SQL account called “episerver” which is an admin on the local SQL installation, all with the same password. From there, we usually call the database episerver.projectname
. Regardless of your setup, just make sure you import the production database and configure your connection string in your web.config accordingly.
This is a little hidden step that I think trips a lot of people up. We automate the installation of code through our devops platform of choice (e.g., Azure Devops). As part of this setup, we always introduce assembly versioning, so each new version of the application gets an automatically assigned and incrementing version number. These version numbers get stored within Epi’s database, and Epi will avoid updating ContentTypes which have a lower version than those found within the database. To correct this, set the version back to 1.0.0.0 (or whatever is in your local AssemblyInfo.cs file). Here’s the SQL script to do it (replace MySite
and x.y.z.0
accordingly):
UPDATE [episerver.mysite].[dbo].[tblContentType] SET ModelType = REPLACE(ModelType, 'x.y.z.0', '1.0.0.0'), Version = '1.0.0.0' WHERE ModelType like '%MySite%'
Now that you have the content, you need the Blobs to go with it. This step is fairly simple- we typically opt to store Blobs in the “For This Site” or “For All Sites” area. Login to production, navigate to the admin tab, and initiate a content export of the necessary roots. From here, log back into your local environment, and import these roots.
You can read about exporting and importing from here.
Don’t forget, because you restored the production database on your local environment, you need to login with your production credentials.
Finally, log back into your local Episerver instance, navigate to the Admin tab, and configure your Site definition with the appropriate hostnames. If you have a wildcard hostname in production, you may not need to do this- but I recommend doing this to be thorough.
And that’s it! Keeping your local environment in sync with production is vital to being an effective developer on an Epi project. Hope this helps, happy coding.
]]>In modern day CMS systems we rarely create pages that have fixed layouts with zero flexibility. Often times we give the marketing team the power to define experiences by placing a number of configurable components on the page in the layout that they see fit. This is empowering, but can also be intimidating if you’re constantly starting with a blank slate. Branch Templates are a concept familiar in other CMS systems that allow developers to define page types that come with pre-existing components whenever a new page is created. This concept is also available to Episerver if you know where to look.
When creating pages in Episerver, they would typically come with no pre-defined blocks. When building pages, this can lead to difficulty as you need to define each and every block that the page requires. Of course, as developers we can hardcode a few elements such as heroes onto the page itself, but this leads to inflexibility and puts you down the path of a very static look and feel. Ontop of this, hardcoded page elements lose the flexibility of Episerver’s block paradigm.
To work around this, we can utilize Episerver’s event system to define a number of blocks that the page should be created with by default. This has the benefit of allowing for a much more streamlined content population experience, while still maintaining the flexibility and power of Episerver’s block system. This looks like a much more convenient page to populate content on, right?
To pull this off, first we must define and tap into the Created event. To do this, I create an Initialization Module like so:
[InitializableModule] [ModuleDependency(typeof(EPiServer.Web.InitializationModule))] public class EventsInitialization : IInitializableModule { public void Initialize(InitializationEngine context) { var events = context.Locate.ContentEvents(); events.CreatedContent += CreatedContentEvent; events.CreatingContent += CreatingContentEvent; } public void Uninitialize(InitializationEngine context) { var events = context.Locate.ContentEvents(); events.CreatingContent -= CreatingContentEvent; events.CreatedContent -= CreatedContentEvent; } private void CreatingContentEvent(object sender, ContentEventArgs e) { if (e.Content is ICreateEvent page) { page.OnCreating(sender, e); } } private void CreatedContentEvent(object sender, ContentEventArgs e) { if (e.Content is ICreateEvent page) { page.OnCreated(sender, e); } } } public interface ICreateEvent { void OnCreated(object sender, ContentEventArgs e); void OnCreating(object sender, ContentEventArgs e); }
Now, any time an item is created, I check if it implements ICreateEvent
. If the item does, I call the corresponding OnCreated
and OnCreating
events. This is a pretty slick way of handling Episerver events that Daved Artemik shared with me earlier this year. You could put the creation logic in this initialization module, but I don’t find that to be a very SOLID approach.
After that is set up, we simply need to have our page type implement ICreateEvent
and implement the methods accordingly. In general, your page should have one or more content areas which you are going to be placing blocks into programatically. Here’s a simplified version of my page:
[ContentType( GroupName = "General Pages", DisplayName = "Standard Content Page", GUID = "28f6892f-f86d-4154-89d5-dfeaef7cae2c", Description = "Standard content pages come with a Hero and Content Spot")] public class StandardContentPage : PageData, ICreateEvent { [Display( GroupName = SystemTabNames.Content, Name = "Main Content Area", Order = 320)] // in my case, IStripeBlock and IPageContentBlock are interface types that the SCORE Block Library define and implement // this allows us to create new types of blocks which should be available to be inserted everywhere, without defining // the AllowedTypes in all locations. IStripeBlock is for edge to edge content, IPageContentBlock is for container // bound elements in CSS [AllowedTypes(typeof(IStripeBlock), typeof(IPageContentBlock))] public virtual ContentArea MainContentArea { get; set; } public void OnCreated(object sender, ContentEventArgs e) { // dependencies var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>(); var contentAssetHelper = ServiceLocator.Current.GetInstance<ContentAssetHelper>(); var blockService = ServiceLocator.Current.GetInstance<IBlockService>(); // folder where all blocks should be created ContentAssetFolder folder = contentAssetHelper.GetOrCreateAssetFolder(ContentLink); // create hero and content spot var hero = blockService.CreateBlock<HeroBlock>(folder.ContentLink); var contentSpot = blockService.CreateBlock<ContentSpotBlock>(folder.ContentLink); // add blocks to content area var clone = CreateWritableClone() as StandardContentPage; if (clone.MainContentArea == null) clone.MainContentArea = new ContentArea(); clone.MainContentArea.Items.Add(new ContentAreaItem { ContentLink = hero.ContentLink }); clone.MainContentArea.Items.Add(new ContentAreaItem { ContentLink = contentSpot.ContentLink }); // save page back contentRepository.Save(clone, SaveAction.Default, AccessLevel.NoAccess); } public void OnCreating(object sender, ContentEventArgs e) { } }
And IBlockService is a simple helper service to remove some boilerplate code:
public class BlockService : IBlockService { private readonly IContentRepository _contentRepository; private readonly IContentTypeRepository _contentTypeRepository; public BlockService(IContentRepository contentRepository, IContentTypeRepository contentTypeRepository) { _contentRepository = contentRepository; _contentTypeRepository = contentTypeRepository; } public IContent CreateBlock<T>(ContentReference parentFolder) { var type = _contentTypeRepository.Load<T>(); var block = _contentRepository.GetDefault<IContent>(parentFolder, type.ID); block.Name = type.DisplayName; _contentRepository.Save(block, SaveAction.Publish, AccessLevel.NoAccess); return block; } }
Now, any time we create pages of this type we receive pre-defined blocks. Most importantly, the hero isn’t hardcoded to the page so we gain the ability to personalize it per visitor group. This is extremely powerful, and I encourage you to take this approach for all page types within your system. Your content authors will thank you!
]]>Often times when building content models inside of a CMS, it’s important to consider the various types of editors and the permissions they may have within the eco-system. One technique that I find highly effective is to limit the amount of fields a user may see based on their CMS role. Removing non-essential fields from your every day content authors leads to system robustness and higher adoptability. In a lot of cases, these fields need to be locked down to special administrative level roles. Here’s how you can do it within Episerver.
First of all, defining a field within Episerver is quite simple. To do it, you simply need to add properties to your corresponding model. For instance, your Home Page may be defined like this:
[ContentType (GUID = "49fda103-1b15-4d1c-81bd-32263cf0d46d")] public class HomePage : EPiServer.Core.PageData { [Display (Name = "Header Content Area")] public virtual ContentArea HeaderContentArea { get; set; } [Display (Name = "Body Content Area")] public virtual ContentArea BodyContentArea { get; set; } [Display (Name = "Footer Content Area")] public virtual ContentArea FooterContentArea { get; set; } }
In this case I’ve defined a page type with 3 fields: Header Content Area, Body Content Area, and Footer Content Area. I do find it beneficial to create the header and footer as dedicated content areas that receive specialized blocks, but more on this later.
Now, let’s say we want to lock down our Header and Footer content areas to specific roles. This can easily be done by creating an IMetadataAware
attribute.
public class EditingRestrictionAttribute : Attribute, IMetadataAware { public EditingRestrictionAttribute() { } public void OnMetadataCreated(ModelMetadata metadata) { // interrogate the EPiServer.Security.PrincipalInfo object to determine if the // current user has access to edit this field if (EPiServer.Security.PrincipalInfo.CurrentPrincipal.IsInRole("CmsAdmins")) { return; } // if user failed the above check, set the field to readonly and hide it for editing mode metadata.IsReadOnly = true; metadata.ShowForEdit = false; } }
The cool part about this is that you can create complex authorization rules. You could interrogate the page and check it’s location within the tree, check it’s type, compare against the user, etc. After your logic is in place, you can simply update your models to use your new attribute:
[ContentType (GUID = "49fda103-1b15-4d1c-81bd-32263cf0d46d")] public class HomePage : EPiServer.Core.PageData { [Display (Name = "Header Content Area")] [EditingRestriction] public virtual ContentArea HeaderContentArea { get; set; } [Display (Name = "Body Content Area")] public virtual ContentArea BodyContentArea { get; set; } [Display (Name = "Footer Content Area")] [EditingRestriction] public virtual ContentArea FooterContentArea { get; set; } }
Hope this helps!
]]>While iterating on projects it’s always a good idea to keep your lower environments in sync with the latest content and imagery from your production environment. In some platforms this is a significant effort, but with Episerver DXP it can be easily automated!
Eric Markson from Perficient recently blogged about how to automate the orchestration of Episerver DXP. Following in his footsteps, I’ve been able to successfully synchronize my Pre-production and Integration environments with production content and images in an automated fashion. The process is quite simple.
First, we need to use the PromoteToEnvironment.ps1 script which you can fork or clone from here: https://github.com/ericmarkson/EpiCloud-Scripts. If you want to learn more about this script, follow along on Eric’s blogs. The jist of it though, is that we can call this with source and target environments with a number of switches.
In general, this script would normally be used to promote changes up. Integration -> Pre-prod, Pre-prod -> Prod, etc. The API also allows you to select what types of elements to promote: Code, Database, Blobs, etc. What many people (including myself) don’t initially realize is: You can promote downward and skip the code.
This means that we can create two sets of automation:
Setting this up is also incredibly simple within a tool like Azure Devops:
In the above screenshot, I’ve created a release pipeline tied back to my application’s code base. The application has the PromoteToEnvironment.ps1 script checked into source, which is used as an artifact on this release. I schedule this release to run over the weekend, but really it could be executed every night if you wish. For us, it takes about 30 minutes to execute per environment.
The job itself is quite simple as well:
The parameters here are also quite simple:
I personally thought this was really cool (borderline amazing) after working in other platforms where this type of task is nearly impossible. It goes to show just how powerful the Episerver DXP platform really is. Enjoy!
]]>Lazy loading images is a technique for modern web developers where you instruct the client’s browser to only download images as they are needed. This leads to tremendous performance improvements, as client devices do not waste bandwidth downloading assets which are not being rendered. To achieve this, we’ll use some client side mechanisms from css-tricks.com and support them with proper markup from the Episerver CMS.
When talking about Imagery used on websites, we need to think of ways in which this imagery is actually used. A foreground image is your traditional image tag <img>
which you learn in HTML 101. This is the standard way in which an image is rendered on screen.
Background imagery, however, is an image which is loaded via CSS and applied through styling. Think of a <div>
tag with a background-image
property inlined on it. This is typically used for large banner imagery with text over-lays.
In both cases, what we need to do is craft our HTML response coming from the server to support a lazy image processing technique in Javascript.
The standard way in which you would serve up an image on your website would be to implement an <img>
tag, like so:
<img src="/path/to/my/image/file.png" />
The unfortunate reality behind this image tag, is that as soon as the client’s browser sees it- it will attempt to load the asset at the src
attribute. Even if this image is located in the footer of a lengthy page- it will be downloaded immediately.
To prevent this download, what we can do is output the markup in a different manner from the server side:
<img data-src="/path/to/my/file.png" class="lazy" />
Notice in this case that we didn’t output a src property at all. Instead, we output data-src. The browser doesn’t know how to handle this, and typically it will render this img tag as a “broken thumbnail” (if we allowed it). Because we put a CSS class of .lazy
on the image, we can style this image to be hidden just-in-case someone scrolls it into view without Javascript.
Within the Head section of your document, add this to your Critical CSS:
<style type="text/css"> img.lazy { opacity: 0; transition: opacity ease-out 0.1s; } .lazy { background-image: none !important; transition: background-image ease-out 0.1s; } </style>
Now comes the fun part- we need to execute a bit of client side Javascript to actually process our images and trick the browser into loading them as they scroll into view. To do this, use a code snippet like this:
function lazyload() { if (lazyloadThrottleTimeout) { clearTimeout(lazyloadThrottleTimeout); } lazyloadThrottleTimeout = setTimeout(function () { var lazyElements = document.querySelectorAll(".lazy"); var scrollTop = window.pageYOffset; Array.prototype.forEach.call(lazyElements, function (elem) { // within 100px of the bottom of the screen if (elem.offsetTop - 100 < (window.innerHeight + scrollTop)) { if (elem.dataset.src) { // find any data-src and switch it to src elem.src = elem.dataset.src; } elem.classList.remove('lazy'); } }); if (lazyElements.length == 0) { document.removeEventListener("scroll", lazyload); window.removeEventListener("resize", lazyload); window.removeEventListener("orientationChange", lazyload); } }, 20); } document.addEventListener("scroll", lazyload); window.addEventListener("resize", lazyload); window.addEventListener("orientationChange", lazyload); lazyload(); // go ahead and invoke on page load
In this case, we’re defining a function lazyload()
which is used as a callback for the scroll
, resize
, and orientationChange
events. We also directly invoke this function on page load, just in case any deferred images are located above-the-fold. All this Javascript does is loop through the DOM and find every element with a class of lazy
. For each object found, if it is within the viewport, the lazy
class is removed and data-src
is swapped for src
. This is the core of lazy loading images, and from here on out we just need to craft our server side responses accordingly. As an interesting side note- it can be incredibly difficult to determine the client device’s viewport from the server side, so I recommend only lazy loading images which are located after the first ~1000px of screen height.
Also take note, that for our background images it’s the same technique. We need to take this markup:
<div style="background-image: url('/path/to/some/banner/image.jpg')"> <!-- ... --> </div>
and instead, render it like so:
<div class="lazy" style="background-image: url('/path/to/some/banner/image.jpg')"> <!-- ... --> </div>
With the CSS we inlined in the head of our documents, the .lazy
class will take precedence and force the background image property to none !important;
.
Up until this point we’ve talked about how to get lazy loading to work on the front-end. This technique applies to a large variety of web technologies, and can be done with a multitude of platforms. All we have to do is get the back-end to render the markup accordingly.
Luckily within Episerver, controlling the way in which Images render is incredibly easy. First, we must ensure we have a DisplayTemplate created for our Image files: ~/Views/Shared/DispalyTemplates/Image.cshtml
. This file will instruct Episerver on how to render the markup for an Image. In my case, Image.cshtml
looks like this (assuming we’re following along with Alloy…):
@using EPiServer.Editor @model ImageViewModel @if (Model != null) { var lazy = ViewBag.lazy ?? true; if (PageEditing.PageIsInEditMode || !lazy) { <img src="@Model.Url" /> } else { <img data-src="@Model.Url" class="lazy" /> } }
The critical piece here is that I’m passing a flag of “lazy” into the ViewBag to control if the image should be lazily loaded or not. This is important for elements such as Heroes, where they are always present above-the-fold and you want them to load as fast as possible.
Here’s how you can use it:
@* Won't be lazy loaded... *@ @Html.PropertyFor(x => x.CriticalImage, new { lazy = false }) @* Will be lazy loaded... *@ @Html.PropertyFor(x => x.LazyImage)
One thing to take note of: the Episerver Rich Text TinyMCE editor doesn’t use the Display Templates for rendering imagery. It has it’s own internal mechanisms to do this. Luckily, we can get around this by applying a similar technique- creating a Display Template for XhtmlString
property types.
Create ~/Views/Shared/DispalyTemplates/XhtmlString.cshtml
to gain control of the way rich text is rendered, and add some code similar to this:
@using EPiServer.Core @model XhtmlString @Html.Raw(Model.FormatRichText(ViewContext))
From here, our FormatRichText()
extension method can handle rendering the markup, grabbing the result, and adjusting it before sending it out to the client. I use HtmlAgilityPack to parse the HTML result. Here’s my extension method:
public static class XhtmlStringExtensions { /// <summary> /// Parses the XhtmlString for Image tags and sets them to lazy load /// </summary> public static string FormatRichText(this XhtmlString html, ViewContext context) { // html is null when the TinyMce is not initialize (creating new block etc.) if (html == null) return string.Empty; // Load up Epi's HtmlHelper and ask it to render results var hh = new HtmlHelper(context, new ViewPage()); string epiRenderingResult; using (var writer = new StringWriter()) { hh.ViewContext.Writer = writer; hh.RenderXhtmlString(html); writer.Flush(); epiRenderingResult = writer.ToString(); } if (PageEditing.PageIsInEditMode) return epiRenderingResult; // once results are rendered, load up HtmlAgilityPack and have it parse results var doc = new HtmlDocument(); doc.LoadHtml(epiRenderingResult); // find all image tags, if there are none just return out- otherwise we need to adjust them var imgs = doc.DocumentNode.SelectNodes("//img"); if (imgs == null) return epiRenderingResult; // for each image, swap src for data-src and throw "lazy" class on it foreach (var img in imgs) { var src = img.Attributes["src"]?.Value; if (!string.IsNullOrEmpty(src)) { // to support lazy loading, we need to swap src for data-src // and inject a class of "lazy". Javascript will take it from there img.SetAttributeValue("data-src", src); img.Attributes.Remove("src"); var css = img.Attributes["class"]?.Value; img.SetAttributeValue("class", $"lazy {css}".Trim()); } } var outerHtml = doc.DocumentNode.OuterHtml; return outerHtml; // return out the new resulting HTML } }
Enjoy
Last week I received a nasty bug report regarding Accessibility and Episerver. Within Rich Text areas in Episerver, the file name is injected by default for alternate text. This hurts your accessibility score and is a detrimental impact to visually impaired users. Coincidentally, others in the community have written about and questioned how to solve this very issue:
I can’t take full credit for my solution because Tomas Hensrud Gulla’s solution provided me with the secret sauce: parsing XhtmlString fragments. His blog post inspired my solution. Also keep in mind that this behavior is recognized by Episerver as a bug, and may be fixed in future releases: https://world.episerver.com/support/Bug-list/bug/CMS-16633
If you’re new to Episerver, or have been working in the platform for some time, you’ll inevitably come across the need to display Alternate text on Image assets. Alternate text is a mechanism for providing additional metadata about imagery to visually impaired users as well as providing some minimal SEO benefit for search engines. If you search around or take a look at Alloy (one of the Episerver demo sites), you’ll see a pretty standard approach to providing alternate text in images:
If you follow along in Alloy, you can see this being done here:
The issue arises when your content authors want to put Images inside of Rich Text fields. If you’re using XhtmlString’s (rich text) on your Episerver solution, one of the default options is to allow either drag and drop of Media assets into the rich text editor, or to allow users to select an image through specific TinyMCE controls. When this happens, however, Episerver uses the Name of the image (defaulted to the file name at upload time) as the alternate text. This results in markup which doesn’t help accessible users at all: <img src="somefile.jpg" alt="somefile.jpg" />
The solution is quite simple. At render time, we can intercept XhtmlString (the backing property type for rich text), and modify what is rendered. Sounds easy enough, right? Here’s how to do it…
First, create a Display Template for XhtmlString.cshtml, and ensure that this is wired up properly for your solution:
@using EPiServer.Core @model XhtmlString @Html.Raw(Model.FixAlternateText(ViewContext))
Secondly, let’s create the extension method for FixAlternateText()
. Within this extension method, we’ll need the ViewContext to be able to ask Episerver to render the results out for modification. This is because Episerver does some magic to apply personalization, render blocks, and so forth within the contents of the XhtmlString fields, and needs the ViewContext to do it. Once we have the Episerver HTML result, we can use HtmlAgilityPack to parse the resulting HTML and adjust it as necessary.
My extension method looks something like this:
public static string FixAlternateText(this XhtmlString html, ViewContext context) { // html is null when the TinyMce is not initialize (creating new block etc.), so just return if (html == null) return string.Empty; // generate a dictionary of <Url, AltText> as strings. Default epi behavior // is to inject the ContentName as the alternate text of the image, and ContentName // defaults to the File Name when uploaded through the UI. This leads to alt text // being set to the file name, which is basically like having no alt text at all. var altTextDictionary = GenerateAltTextDictionary(html.Fragments); // Load up Epi's HtmlHelper and ask it to render results var hh = new HtmlHelper(context, new ViewPage()); string epiRenderingResult; using (var writer = new StringWriter()) { hh.ViewContext.Writer = writer; hh.RenderXhtmlString(html); writer.Flush(); epiRenderingResult = writer.ToString(); } // we don't care about edit mode, just return the HTML result out if (PageEditing.PageIsInEditMode) return epiRenderingResult; // once results are rendered, load up HtmlAgilityPack and have it parse results var doc = new HtmlDocument(); doc.LoadHtml(epiRenderingResult); // find all image tags, if there are none just return out- otherwise we need to adjust them var imgs = doc.DocumentNode.SelectNodes("//img"); if (imgs == null) return epiRenderingResult; // for each image, swap alt="name" for alt="alt text" for accessibility foreach (var img in imgs) { var src = img.Attributes["src"].Value; // if we know this alt text by key, replace it by the key's value // remember, the dictionary contains <Url, AltText> if (altTextDictionary.ContainsKey(src)) { img.SetAttributeValue("alt", altTextDictionary[src]); } } var outerHtml = doc.DocumentNode.OuterHtml; return outerHtml; // return out the new resulting HTML } private static Dictionary<string, string> GenerateAltTextDictionary(StringFragmentCollection htmlFragments) { var _contentLoader = ServiceLocator.Current.GetInstance<IContentLoader>(); // yuck, but what can you do? :) Dictionary<string, string> retVal = new Dictionary<string, string>(); foreach (var urlFragment in htmlFragments.Where(x => x is UrlFragment)) { foreach (var guid in urlFragment.ReferencedPermanentLinkIds) { // assuming our custom ImageData model is called "ImageFile" with a property of "AltText" if (_contentLoader.TryGet(guid, out ImageFile image)) { var key = image.ContentLink.GetPublicUrl(); if (!retVal.ContainsKey(key)) { retVal.Add(key, image.AltText); // you may want to cleanse AltText for proper HTML markup } } } } return retVal; }
After deploying this change up, any time an XhtmlString is rendered, we are now intercepting it and adjusting it’s alternate text accordingly.
Hope this helps you on your Episerver journey!
]]>
When chasing down performance problems on a website, you’ll often times hit an error around deferring offscreen images. This warning occurs when you have imagery “below the fold” (e.g., the area you must scroll to see) loading on your webpages. This problem is especially rampant in CMS systems where you’re never quite sure what the content authoring team is assembling.
Unfortunately, images are critical. We can’t eliminate them. According to the state of images report, at the time of writing, the average webpage loads 29 images at 950kb. The report states that the average webpage can save an additional 300kb of load time if images are lazily loaded.
Luckily, there are a few tricks to solve this within Sitecore, although these principles extend to any website or CMS architecture.
First off, when I talk about foreground imagery, I’m talking about any image loaded directly via an inline <img>
tag within your HTML. This is the simplest kind of image, it’s HTML 101.
Background imagery, on the other hand, is imagery which may be loaded via the CSS background-image property. These are often large banner images used to build logical chunks of your webpages (think stripe component).
In both cases, what we need to do is craft our initial HTML response coming out of the server to have the proper markup so that Javascript can take over and perform the actual “loading” of images for us. This technique is nothing new, and you can read about it in depth on css-tricks.com.
Let’s start with the front-end pieces.
Traditionally, to serve an image you would have HTML output similar to this:
<img src="/path/to/my/image/file.png" />
The unfortunate part about this tag is that the browser will initiate a download of this image as soon as it sees this HTML snippet, regardless of where this tag is on the page. We can prevent that download by transforming the tag slightly:
<img data-src="/path/to/my/image/file.png" class="lazy" />
The data-src treatment will prevent the browser from automatically loading this image. Keep in mind that this will result in what appears to be a broken image if the user actually sees it (unless you style .lazy accordingly). If this is a problem you can also take a secondary, but similar approach:
<img src="/path/to/a/really/small/placeholder.jpg#/path/to/my/real/image.png" class="lazy" />
In this second approach, we’re actually loading a real image (placeholder.jpg), but this “real” image can be as simple as a 1px by 1px white box. The overall method is still the same.
Essentially, whenever the image comes into view, we need to use a bit of javascript to replace “data-src” with “src”. In doing so, this will trigger the browser to download and paint the image immediately.
function lazyload() { if (lazyloadThrottleTimeout) { clearTimeout(lazyloadThrottleTimeout); } lazyloadThrottleTimeout = setTimeout(function () { var lazyElements = document.querySelectorAll(".lazy"); var scrollTop = window.pageYOffset; Array.prototype.forEach.call(lazyElements, function (elem) { // within 100px of the bottom of the screen if (elem.offsetTop - 100 < (window.innerHeight + scrollTop)) { if (elem.dataset.src) { // find any data-src and switch it to src elem.src = elem.dataset.src; } elem.classList.remove('lazy'); } }); if (lazyElements.length == 0) { document.removeEventListener("scroll", lazyload); window.removeEventListener("resize", lazyload); window.removeEventListener("orientationChange", lazyload); } }, 20); } document.addEventListener("scroll", lazyload); window.addEventListener("resize", lazyload); window.addEventListener("orientationChange", lazyload); lazyload(); // go ahead and invoke on page load
Again, this javascript is nothing new. You can see a similar example on css-tricks.com or within this codepen. We’re essentially looping through all elements with the class of “lazy” and processing the ones that are currently visible on the screen. Processing involves swapping data-src for src and removing the class of “lazy”.
Up until this point, everything you’ve seen revolves around pure HTML and JS. Luckily, the Sitecore CMS is flexible enough to allow us full control over how HTML is rendered, so we can absolutely accommodate our new front-end trick. Rather than attempt to manually render images by hand, what we want to do is leverage the existing @Html.Sitecore().Field(…)
helper methods and extend them to always render images with the data-src attribute. To do it, we’ll patch into the renderField
pipeline.
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:role="http://www.sitecore.net/xmlconfig/role/" xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/"> <sitecore> <pipelines> <renderField> <processor role:require="ContentDelivery or Standalone" patch:before="processor[@type='Sitecore.Pipelines.RenderField.GetImageFieldValue, Sitecore.Kernel']" type="MySite.Pipelines.RenderField.LazyImageRenderer, MySite.Custom" /> </renderField> </pipelines> </sitecore> </configuration>
Drop this into your website’s Pipelines.config
file (or make one if you don’t have one). The renderField
pipeline gets executed every time Sitecore attempts to render an image. Each processor within the pipeline checks the field type to render, and generates the necessary HTML if possible. Once HTML is generated, the pipeline is exited. Traditionally, each processor is responsible for a particular field type. In our case, we’re patching a processor in place before the default Sitecore GetImageFieldValue
processor (responsible for generating the <img>
tag markup).
Our processor is quite simple. We can inherit from Sitecore’s GetImageFieldValue
processor to do most of the heavy lifting for us. From there, all we really need to do is alter the results being written to the HTTP response:
public class LazyImageRenderer : Sitecore.Pipelines.RenderField.GetImageFieldValue { public DeferedImageRenderer() { } public virtual void Process(RenderFieldArgs args) { Assert.ArgumentNotNull((object)args, nameof(args)); if (!this.IsImage(args)) // base utility method return; if (this.ShouldExecute()) { base.Process(args); // generate "default" result args.Result.FirstPart = this.FixImageTag(args.Result.FirstPart); // alter results args.AbortPipeline(); // exit out } } public bool ShouldExecute() { if (!Sitecore.Context.PageMode.IsNormal) return false; if (Sitecore.Context.Site == null) return false; if (RenderingContext.Current?.Rendering?.RenderingItem?.InnerItem == null) return false; // note: you have access to RenderingContext.Current.Rendering at this point in time // you could check this for the current placeholder or type of rendering being processed // this may be useful if you want to avoid lazy loading images within your header, or // prevent lazy loading images within your hero renderings. return true; } public string FixImageTag(string tag) { // swap src= for data-src= to trick the browser into ignoring this image tag tag = tag.Replace("src=", "data-src="); // important: inject a class of "lazy" to ensure Javascript can lazily load this image if (tag.Contains("class=\"")) { tag = tag.Replace("class=\"", "class=\"lazy "); } else { tag = tag.Replace("/>", "class=\"lazy\" />"); } return tag; } }
As you can see, we’re simply replacing “src” with “data-src” and injecting the CSS class of “lazy” in place. Easy enough, right?
One important note here: it may or may not be wise for you to defer all images on your website. If you can, try and detect and lazily load only the images which are below the fold. This may not be possible in all scenarios, but checking RenderingContext.Current.Rendering against a list of known Hero renderings is a good way to know if this should or shouldn’t execute. Another option may be to create an “above the fold” placeholder for all pages. If all else fails, it may be wise to lazy load all imagery if your pages are long and this error must be fixed- but if your pages are short and you only have a handful of below the fold images, lazy loading may cause more harm than good.
Also take note that Javascript is required for this to work, so if supporting non-javascript enabled experiences are a must, you shouldn’t implement this (although that could be another check if, perhaps, you check the current HttpContext for a flag set by javascript).
Up until this point, I’ve mostly talked about Foreground imagery. So what about background imagery?
Background imagery simply looks like this:
<div style="background-image: url('/path/to/some/banner/image.jpg')"> <!-- ... --> </div>
This is where the power of CSS comes in really handy. Remember our Javascript snippet earlier? It’s indiscriminately looking for all instances of the class “lazy” on the page, regardless of tag type, and is removing the class as they scroll into view. We can leverage this to our advantage with this CSS:
<style type="text/css"> .lazy { background-image: none !important; transition: background-image ease-out 0.1s; } </style>
Be sure to put this directly within the <head> of your HTML (NOT within a css file), and ensure that this renders before any other images on the page. Forcing background-image: none !important;
will override all other in-lined background-image styles on the website. Whenever the lazy class is removed, the inline style will take over and the image will load. We typically throw a bit of spice on top with the ease-out css as well. This means that our above example simply needs to be modified to:
<div class="lazy" style="background-image: url('/path/to/some/banner/image.jpg')"> <!-- ... --> </div>
Now, another quick tip is to apply a similar technique to your foreground images:
<style type="text/css"> img.lazy { opacity: 0; transition: opacity ease-out 0.1s; } </style>
Put this in the head of your document as well, and it will ensure that any of your lazy images won’t appear “broken” if the user happens to scroll one into view before their network can catch up and load the image.
Again, shout out to css-tricks.com for sharing their wonderful guide on lazy loading, and I hope this helps you within your Sitecore journey.
]]>Responsive Web Design (RWD) offers a cost-effective, high quality and easily managed delivery of content to a variety of devices.
Many teams build sites in RWD by using custom or popular responsive frameworks like Twitter Bootstrap or Foundation Zurb. But how can we combine the responsive design goodness with an intuitive content authoring experience in Episerver?
There is a HUGE difference in the resolution and physical size of a desktop monitor and a mobile phone or tablet.
When adding images to the website, we wish to take advantage of the “real-estate” of the desktop system to add beautiful images that help tell our story. These images can be large, and they set a focal point – a position in the image that is meant to focus the viewer’s attention to a concept or meaning of the depiction.
When we scale the screen down to a smaller device, we really have 3 choices:
Options #1 and #2 do some good in some cases, however, they can easily distort the image or the message of the image by changing its focal point.
Let’s take an example from EpiJuice – here’s a full resolution image for the desktop
Check out this video to see the Responsive Image Cropping in action!
In your custom blocks, you can use a new field type called a PictureField
. Simply give it the ResponsivePictureEditorDescriptor
, and a number of CropPoint
attributes. Each CropPoint
has a width, height, device key, and media query. this is CSharp and usually the language key in code formatters is ‘csharp’
[EditorDescriptor(EditorDescriptorType = typeof(ResponsivePictureEditorDescriptor))]
[CropPoint(768, 512, "Desktop", "(min-width: 992px)")]
[CropPoint(512, 480, "Tablet", "(min-width: 768px) and (max-width: 991px)")]
[CropPoint(512, 256, "Mobile", "(max-width: 767px)")]
public virtual PictureField Image { get; set; }
We provide the field rendering for you, all you need to do is use @Html.PropertyFor(x => x.Image)
within your block’s View file.
This will result in an HTML5 <picture>
tag to be rendered with the appropriate croppings.
When cropping is complete, we save a physically cropped version of the asset for each device viewport specified by the CropPoint
attribute back to Episerver’s media library. This means that the optimal image size is delivered to each device, as specified by each crop point’s media query.
You can associate different CropPoint
attributes with different instances of PictureField
across your blocks. This means each block can have it’s own crop sizes defined. You can also define as many CropPoint attributes as you need.
If you attempt to crop an image that already has existing croppings for this block type, you’ll be prompted to adopt and reuse them. Otherwise, you have the option of regenerating croppings.
]]>Optimal image size is delivered to each device, as specified by each crop point’s media query.