Create Your Own Continuous Scroll in Sitecore
To implement a site with continuous scroll is an interesting challenge that can be solved in different ways. In one of our recent projects, we used Coveo to search for and retrieve articles once a user clicked a button. However, it’s also possible to implement a solution that uses a different method of item retrieval and does so after a different trigger. In this blog post I’m going to run you through a heuristic for implementing a continuous scrolling page in Sitecore, including some specific tools to use.
Here’s roughly what you need to do:
- Create a view rendering separate from the content
- Create a trigger for content retrieval on the page in question
- Query the database for the content
- Display the content on the page
- Re-initialize any specific javascript for the content.
Note: most of the following code was written by or adapted from one of our resident experts, Albraa Nabelsi.
Create a view rendering separate from the content
Say you have an Article Page template which all Article items use. If you navigated directly an Article item, you would see the layout configured on the Standard Values of the Article Page template. In the layout for the Article Page Standard Values item, you would have a rendering for the article, and then immediately after it you would insert the rendering for the continuous/infinite content. This is because you need a “root div” to insert retrieved content in/around/before. In our case we created a view and used the following root div:
<div id="insert-div"></div>
This is where our content will insert.
Create a trigger for content retrieval on the page in question
After your root div, you’ll put the code for the trigger to retrieve more content. The triggers you’ll likely use for an infinite/continuous scrolling page will be either scroll- or button-based. In our last solution we used a nice Load More button:
Buttons like this are easy to implement, and their corresponding javascript functions are bound easily, either inline or through a jquery statement like:
$('.load_more_btn').on('click', function () {
//Insert content retrieval AJAX query here
});
However, we could also have loaded the articles automatically, based on when the user got to a certain point on the page. To do so, we would use a technology like the new Intersection Observer API, which allows developers to track when specific elements enter the viewport, as well as how much of the element is in the viewport.
To briefly explain how Intersection Observer works: after importing IntersectionObserver into your project (don’t forget the polyfill for the older browsers!), you need to create an IntersectionObserver object for whatever element(s) you want to track. In our instance, we want to track a div that triggers our content retrieval when it enters the browser viewport. We might do it like this:
<div id="continuous-trigger"></div>
<div id="insert-div"></div>
<script>
function continuousTrigger(entries, observer){
for (var i = 0; i < entries.length; i++) {
if (entries[i].isIntersecting) {
var element = entries[i].target;
//content retrieval AJAX query
retrieveContent('#insert-div');
}
}
}
const options = {
threshold: [0.1] //trigger when even 10% of the element is in the viewport
};
var observer = new IntersectionObserver(continuousTrigger, options);
observer.observe($('#continuous-trigger')[0]); //accepts an element
</script>
You can test using this code in the console, just make sure to put a “console.log()” statement in the continuousTrigger function.
Query the database for the content
Upon triggering the content retrieval code, we have to, well, retrieve the content. As I said above, there are a few ways to do this. In our last solution we used Coveo to query against the Coveo index to get random article item ids, then we ran an AJAX call to retrieve the markup for those items. Technically you don’t need Coveo to do this - you could instead use Sitecore’s search API in the function called by the AJAX call.
First, let’s set up the AJAX call. In the above code I referenced a function called retrieveContent(), so let’s write that.
retrieveContent(entityName){
$.ajax({
type: "GET",
url: "/api/Continuous/getArticleContent",
dataType: "html",
success: function(response){
//We'll write the rest of this function in a second
},
error: function(xhr, status, error) {
var err = eval("(" + xhr.responseText + ")"); console.log(err.Message);
}
}
Next, we need to make sure that Continuous/getArticleContent is an available function. We created a project in our Helix solution called [Customer].Feature.Continuous. In that project, we created a controller class in a “Controllers” folder called “ContinuousController.” In that class we created a public function called getArticleContent. We’ll do something like that here:
namespace [Customer].Feature.Continuous.Controllers
{
public class ContinuousController : Controller
{
[HttpGet]
public string getArticleContent()
{
string returnHtml = "";
//TODO: YOU WRITE HERE: perform a Sitecore search and find whatever type of item you want and assign its ID to queryResult
//You could even retrieve and concatenate multiple pages
//var queryResult =
var itemID = queryResult;
var item = Sitecore.Context.Database.GetItem(Sitecore.Data.ID.Parse(itemID));
var url = LinkManager.GetItemUrl(item, new UrlOptions() { AlwaysIncludeServerUrl = true, LanguageEmbedding = LanguageEmbedding.Always, Site = Sitecore.Context.Site });
//Get the rendered item as a string
returnHtml = Sitecore.Web.WebUtil.ExecuteWebPage(url);
return returnHtml;
}
}
}
Aside from the search functionality (I believe in you!), the important part of this code is the Sitecore.Web.WebUtil.ExecuteWebPage() call. ExecuteWebPage(url) is a function that runs an item through the layout engine, returning output HTML as if you had navigated there by URL. It will utilize whatever layout is attached to it.
After writing this function, we need to set up the MVC routes. First, create a “Pipeline” folder in your Continuous project, and create a new class in it called InitRoutes.cs. Here’s the route code:
using System.Web.Mvc;
using System.Web.Routing;
using Sitecore.Pipelines;
namespace [Customer].Feature.Continuous.Pipeline
{
public class InitRoutes : Sitecore.Mvc.Pipelines.Loader.InitializeRoutes
{
public override void Process(PipelineArgs args)
{
RouteTable.Routes.MapRoute(
"[Customer].Feature.Continuous", // Route name
"api/Continuous/{action}", // URL with parameters
new { controller = "Continuous", action = "getArticleContent" },
new[] { "[Customer].Feature.Continuous.Controllers" });
}
}
}
And finally, create a new config file in your project, in App_Config/Include/Feature/Continuous. Call it [Customer].Feature.Continuous.Route.config.
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<pipelines>
<initialize>
<processor type="[Customer].Feature.Continuous.Pipeline.InitRoutes, [Customer].Feature.Continuous"
patch:before="processor[@type='Sitecore.Mvc.Pipelines.Loader.InitializeRoutes, Sitecore.Mvc']"/>
</initialize>
</pipelines>
</sitecore>
</configuration>
Display the content on the page
Once you write whatever search you want to get your object, retrieve your object, render the layout for that object, you’ll send that rendered HTML back to the AJAX call as a response.
The important part of the response HTML exists within a div of class ‘article-content’, so we first assign that div to a “dom” variable, then insert “dom” right before our “insert-div” div. After doing so, we’ll run the scripts in all of the <script> tags inside “dom”.
retrieveContent(entityName){
$.ajax({
type: "GET",
url: "/api/Continuous/getArticleContent",
dataType: "html",
success: function(response){
//The following assumes all article content is in a div with class 'article-content'
var dom = $($($.parseHTML(response, true)).find('.article-content'));
dom.insertBefore(entityName);
//The following executes <script> tags in response HTML
dom.find('script').each(function () {
$.globalEval(this.text || this.textContent || this.innerHTML || '');
});
},
error: function(xhr, status, error) {
var err = eval("(" + xhr.responseText + ")"); console.log(err.Message);
}
}
Re-initialize any specific javascript for the content
In our last project, the articles we loaded through continuous scroll had a few third-party Javascript plugins, including Disqus and Google Publisher Tags. Each one works slightly differently, but requires a custom implementation for continuous scroll. I’ll briefly explain the challenges and general solutions for each.
Disqus
Disqus was a bit of a tough nut to crack. They have a post about using Disqus through AJAX, but it doesn’t cover how it’s initialized. Disqus is initialized with a javascript function which uses a disqus_config object that can be configured with a URL and/or ID that corresponds to a specific chat window. This function loads scripts in the <head> tag, which then load the appropriate comment window based on the config object. However, there can only be ONE Disqus comment window on a page at one time. That’s where the Disqus.reset function comes in handy. I used another IntersectionObserver to reset Disqus whenever a user scrolls to the end of an article. The proper information for the Disqus reset came from a hidden div attached to the article markup.
Google Publisher Tags
Our client needed a custom solution for ads, so we elected to use Google Ad Manager instead of Adsense. This solution let our client make their own ad campaigns and take orders for others. However, we needed responsive ads and ads to repeat on continuous scroll. Fortunately, Google provides useful example code that we heavily modified. Once again, we used IntersectionObservers to decide when to call new ads. We also kept track of the amount of ad slots on the page, and destroyed any past a certain amount, since Google will only render the amount of images available to an ad unit.
Conclusion:
With AJAX and Sitecore MVC, we can create pages that continuously load content. The primary technologies of our Sitecore solution for continuous scroll are the Intersection Observer API, Javascript and Sitecore MVC for AJAX, a search engine to retrieve an item or item ID, and Sitecore’s ExecuteWebPage() WebUtil function. We use these to trigger an AJAX call to retrieve markup and then insert that markup on the page.
There are numerous ways one can implement continuous/infinite scroll, so I advise you to experiment and figure out what’s right for your specific solution.
If you have any questions/comments/experience, please feel free to share in the comments!