Cross-site publishing alternatives in SharePoint Online/Office 365
Cross-site publishing is one of the powerful new capabilities in SharePoint 2013. It enables the separation of data entry from display and breaks down the container barriers that have traditionally existed in SharePoint (ex: rolling up information across site collections). Cross-site publishing is delivered through search and a number of new features, including list/library catalogs, catalog connections, and the content search web part. Unfortunately, SharePoint Online/Office 365 doesn’t currently support these features. Until they are added to the service (possibly in a quarterly update), customers will be looking for alternatives to close the gap. In this post, I will outline several alternatives for delivering cross-site and search-driven content in SharePoint Online and how to template these views for reuse. Here is a video that outlines the solution:
[View:https://www.youtube.com/watch?v=chwHhEmIERg]
NOTE: I’m a huge proponent of SharePoint Online. After visiting several Microsoft data centers, I feel confident that Microsoft is better positioned to run SharePoint infrastructure than almost any organization in the world. SharePoint Online has very close feature parity to SharePoint on-premise, with the primary gaps existing in cross-site publishing and advanced business intelligence. Although these capabilities have acceptable alternatives in the cloud (as will be outlined in this post), organizations looking to maximize the cloud might consider SharePoint running in IaaS for immediate access to these features. |
Apps for SharePoint
The new SharePoint app model is fully supported in SharePoint Online and can be used to deliver customizations to SharePoint using any web technology. New SharePoint APIs can be used with the app model to deliver an experience similar to cross-site publishing. In fact, the content search web part could be re-written for delivery through the app model as an “App Part” for SharePoint Online.
Although the app model provides great flexibility and reuse, it does come with some drawbacks. Because an app part is delivered through a glorified IFRAME, it would be challenging to navigate to a new page from within the app part. A link within the app would only navigate within the IFRAME (not the parent of the IFRAME). Secondly, there isn’t a great mechanism for templating a site to automatically leverage an app part on its page(s). Apps do not work with site templates, so a site that contains an app cannot be saved as a template. Apps can be “stapled” to sites, but the app installed event (which would be needed to add the app part to a page) only fires when the app is installed into the app catalog.
REST APIs and Script Editor
The script editor web part is a powerful new tool that can help deliver flexible customization into SharePoint Online. The script editor web part allows a block of client-side script to be added to any wiki or web part page in a site. Combined with the new SharePoint REST APIs, the script editor web part can deliver mash-ups very similar to cross-site publishing and the content search web part. Unlike apps for SharePoint, the script editor isn’t constrained by IFRAME containers, app permissions, or templating limitations. In fact, a well-configured script editor web part could be exported and re-imported into the web part gallery for reuse.
Cross-site publishing leverages “catalogs” for precise querying of specific content. Any List/Library can be designated as a catalog. By making this designation, SharePoint will automatically create managed properties for columns of the List/Library and ultimately generate a search result source in sites that consume the catalog. Although SharePoint Online doesn’t support catalogs, it support the building blocks such as managed properties and result sources. These can be manually configured to provide the same precise querying in SharePoint Online and exploited in the script editor web part for display.
Calling Search REST APIs
<div id="divContentContainer"></div><script type="text/javascript"> $(document).ready(function ($) { var basePath = "https://tenant.sharepoint.com/sites/somesite/_api/"; $.ajax({ url: basePath + "search/query?Querytext='ContentType:News'", type: "GET", headers: { "Accept": "application/json;odata=verbose" }, success: function (data) { //script to build UI HERE }, error: function (data) { //output error HERE } }); });</script> |
An easier approach might be to directly reference a list/library in the REST call of our client-side script. This wouldn’t require manual search configuration and would provide real-time publishing (no waiting for new items to get indexed). You could think of this approach similar to a content by query web part across site collections (possibly even farms) and the REST API makes it all possible!
List REST APIs
<div id="divContentContainer"></div><script type="text/javascript"> $(document).ready(function ($) { var basePath = "https://tenant.sharepoint.com/sites/somesite/_api/"; $.ajax({ url: basePath + "web/lists/GetByTitle('News')/items/?$select=Title&$filter=Feature eq 0", type: "GET", headers: { "Accept": "application/json;odata=verbose" }, success: function (data) { //script to build UI HERE }, error: function (data) { //output error HERE } }); });</script> |
The content search web part uses display templates to render search results in different arrangements (ex: list with images, image carousel, etc). There are two types of display templates the content search web part leverages…the control template, which renders the container around the items, and the item template, which renders each individual item in the search results. This is very similar to the way a Repeater control works in ASP.NET. Display templates are authored using HTML, but are converted to client-side script automatically by SharePoint for rendering. I mention this because our approach is very similar…we will leverage a container and then loop through and render items in script. In fact, all the examples in this post were converted from display templates in a public site I’m working on.
Item display template for content search web part
<!--#_var encodedId = $htmlEncode(ctx.ClientControl.get_nextUniqueId() + "_ImageTitle_");var rem = index % 3;var even = true;if (rem == 1) even = false; var pictureURL = $getItemValue(ctx, "Picture URL");var pictureId = encodedId + "picture";var pictureMarkup = Srch.ContentBySearch.getPictureMarkup(pictureURL, 140, 90, ctx.CurrentItem, "mtcImg140", line1, pictureId);var pictureLinkId = encodedId + "pictureLink";var pictureContainerId = encodedId + "pictureContainer";var dataContainerId = encodedId + "dataContainer";var dataContainerOverlayId = encodedId + "dataContainerOverlay";var line1LinkId = encodedId + "line1Link";var line1Id = encodedId + "line1"; _#--><div style="width: 320px; float: left; display: table; margin-bottom: 10px; margin-top: 5px;"> <a href="_#= linkURL =#_"> <div style="float: left; width: 140px; padding-right: 10px;"> <img src="_#= pictureURL =#_" class="mtcImg140" style="width: 140px;" /> </div> <div style="float: left; width: 170px"> <div class="mtcProfileHeader mtcProfileHeaderP">_#= line1 =#_</div> </div> </a></div> |
Script equivalent
<div id="divUnfeaturedNews"></div><script type="text/javascript"> $(document).ready(function ($) { var basePath = "https://richdizzcom.sharepoint.com/sites/dallasmtcauth/_api/"; $.ajax({ url: basePath + "web/lists/GetByTitle('News')/items/?$select=Title&$filter=Feature eq 0", type: "GET", headers: { "Accept": "application/json;odata=verbose" }, success: function (data) { //get the details for each item var listData = data.d.results; var itemCount = listData.length; var processedCount = 0; var ul = $("<ul style='list-style-type: none; padding-left: 0px;' class='cbs-List'>"); for (i = 0; i < listData.length; i++) { $.ajax({ url: listData[i].__metadata["uri"] + "/FieldValuesAsHtml", type: "GET", headers: { "Accept": "application/json;odata=verbose" }, success: function (data) { processedCount++; var htmlStr = "<li style='display: inline;'><div style='width: 320px; float: left; display: table; margin-bottom: 10px; margin-top: 5px;'>"; htmlStr += "<a href='#'>"; htmlStr += "<div style='float: left; width: 140px; padding-right: 10px;'>"; htmlStr += setImageWidth(data.d.PublishingRollupImage, '140'); htmlStr += "</div>"; htmlStr += "<div style='float: left; width: 170px'>"; htmlStr += "<div class='mtcProfileHeader mtcProfileHeaderP'>" + data.d.Title + "</div>"; htmlStr += "</div></a></div></li>"; ul.append($(htmlStr)) if (processedCount == itemCount) { $("#divUnfeaturedNews").append(ul); } }, error: function (data) { alert(data.statusText); } }); } }, error: function (data) { alert(data.statusText); } }); }); function setImageWidth(imgString, width) { var img = $(imgString); img.css('width', width); return img[0].outerHTML; }</script> |
Even one of the more complex carousel views from my site took less than 30min to convert to the script editor approach.
Advanced carousel script
<div id="divFeaturedNews"> <div class="mtc-Slideshow" id="divSlideShow" style="width: 610px;"> <div style="width: 100%; float: left;"> <div id="divSlideShowSection"> <div style="width: 100%;"> <div class="mtc-SlideshowItems" id="divSlideShowSectionContainer" style="width: 610px; height: 275px; float: left; border-style: none; overflow: hidden; position: relative;"> <div id="divFeaturedNewsItemContainer"> </div> </div> </div> </div> </div> </div></div><script type="text/javascript"> $(document).ready(function ($) { var basePath = "https://richdizzcom.sharepoint.com/sites/dallasmtcauth/_api/"; $.ajax({ url: basePath + "web/lists/GetByTitle('News')/items/?$select=Title&$filter=Feature eq 1&$top=4", type: "GET", headers: { "Accept": "application/json;odata=verbose" }, success: function (data) { var listData = data.d.results; for (i = 0; i < listData.length; i++) { getItemDetails(listData, i, listData.length); } }, error: function (data) { alert(data.statusText); } }); }); var processCount = 0; function getItemDetails(listData, i, count) { $.ajax({ url: listData[i].__metadata["uri"] + "/FieldValuesAsHtml", type: "GET", headers: { "Accept": "application/json;odata=verbose" }, success: function (data) { processCount++; var itemHtml = "<div class='mtcItems' id='divPic_" + i + "' style='width: 610px; height: 275px; float: left; position: absolute; border-bottom: 1px dotted #ababab; z-index: 1; left: 0px;'>" itemHtml += "<div id='container_" + i + "' style='width: 610px; height: 275px; float: left;'>"; itemHtml += "<a href='#' title='" + data.d.Caption_x005f_x0020_x005f_Title + "' style='width: 610px; height: 275px;'>"; itemHtml += data.d.Feature_x005f_x0020_x005f_Image; itemHtml += "</a></div></div>"; itemHtml += "<div class='titleContainerClass' id='divTitle_" + i + "' data-originalidx='" + i + "' data-currentidx='" + i + "' style='height: 25px; z-index: 2; position: absolute; background-color: rgba(255, 255, 255, 0.8); cursor: pointer; padding-right: 10px; margin: 0px; padding-left: 10px; margin-top: 4px; color: #000; font-size: 18px;' onclick='changeSlide(this);'>"; itemHtml += data.d.Caption_x005f_x0020_x005f_Title; itemHtml += "<span id='currentSpan_" + i + "' style='display: none; font-size: 16px;'>" + data.d.Caption_x005f_x0020_x005f_Body + "</span></div>"; $('#divFeaturedNewsItemContainer').append(itemHtml); if (processCount == count) { allItemsLoaded(); } }, error: function (data) { alert(data.statusText); } }); } window.mtc_init = function (controlDiv) { var slideItems = controlDiv.children; for (var i = 0; i < slideItems.length; i++) { if (i > 0) { slideItems[i].style.left = '610px'; } }; }; function allItemsLoaded() { var slideshows = document.querySelectorAll(".mtc-SlideshowItems"); for (var i = 0; i < slideshows.length; i++) { mtc_init(slideshows[i].children[0]); } var div = $('#divTitle_0'); cssTitle(div, true); var top = 160; for (i = 1; i < 4; i++) { var divx = $('#divTitle_' + i); cssTitle(divx, false); divx.css('top', top); top += 35; } } function cssTitle(div, selected) { if (selected) { div.css('height', 'auto'); div.css('width', '300px'); div.css('top', '10px'); div.css('left', '0px'); div.css('font-size', '26px'); div.css('padding-top', '5px'); div.css('padding-bottom', '5px'); div.find('span').css('display', 'block'); } else { div.css('height', '25px'); div.css('width', 'auto'); div.css('left', '0px'); div.css('font-size', '18px'); div.css('padding-top', '0px'); div.css('padding-bottom', '0px'); div.find('span').css('display', 'none'); } } window.changeSlide = function (item) { //get all title containers var listItems = document.querySelectorAll('.titleContainerClass'); var currentIndexVals = { 0: null, 1: null, 2: null, 3: null }; var newIndexVals = { 0: null, 1: null, 2: null, 3: null }; for (var i = 0; i < listItems.length; i++) { //current Index currentIndexVals[i] = parseInt(listItems[i].getAttribute('data-currentidx')); } var selectedIndex = 0; //selected Index will always be 0 var leftOffset = ''; var originalSelectedIndex = ''; var nextSelected = ''; var originalNextIndex = ''; if (item == null) { var item0 = document.querySelector('[data-currentidx="' + currentIndexVals[0] + '"]'); originalSelectedIndex = parseInt(item0.getAttribute('data-originalidx')); originalNextIndex = originalSelectedIndex + 1; nextSelected = currentIndexVals[0] + 1; } else { nextSelected = item.getAttribute('data-currentidx'); originalNextIndex = item.getAttribute('data-originalidx'); } if (nextSelected == 0) { return; } for (i = 0; i < listItems.length; i++) { if (currentIndexVals[i] == selectedIndex) { //this is the selected item, so move to bottom and animate var div = $('[data-currentidx="0"]'); cssTitle(div, false); div.css('left', '-400px'); div.css('top', '230px'); newIndexVals[i] = 3; var item0 = document.querySelector('[data-currentidx="0"]'); originalSelectedIndex = item0.getAttribute('data-originalidx'); //annimate div.delay(500).animate( { left: '0px' }, 500, function () { }); } else if (currentIndexVals[i] == nextSelected) { //this is the NEW selected item, so resize and slide in as selected var div = $('[data-currentidx="' + nextSelected + '"]'); cssTitle(div, true); div.css('left', '-610px'); newIndexVals[i] = 0; //annimate div.delay(500).animate( { left: '0px' }, 500, function () { }); } else { //move up in queue var curIdx = currentIndexVals[i]; var div = $('[data-currentidx="' + curIdx + '"]'); var topStr = div.css('top'); var topInt = parseInt(topStr.substring(0, topStr.length - 1)); if (curIdx != 1 && nextSelected == 1 || curIdx > nextSelected) { topInt = topInt - 35; if (curIdx - 1 == 2) { newIndexVals[i] = 2 }; if (curIdx - 1 == 1) { newIndexVals[i] = 1 }; } //move up div.animate( { top: topInt }, 500, function () { }); } }; if (originalNextIndex < 0) originalNextIndex = itemCount - 1; //adjust pictures $('#divPic_' + originalNextIndex).css('left', '610px'); leftOffset = '-610px'; $('#divPic_' + originalSelectedIndex).animate( { left: leftOffset }, 500, function () { }); $('#divPic_' + originalNextIndex).animate( { left: '0px' }, 500, function () { }); var item0 = document.querySelector('[data-currentidx="' + currentIndexVals[0] + '"]'); var item1 = document.querySelector('[data-currentidx="' + currentIndexVals[1] + '"]'); var item2 = document.querySelector('[data-currentidx="' + currentIndexVals[2] + '"]'); var item3 = document.querySelector('[data-currentidx="' + currentIndexVals[3] + '"]'); if (newIndexVals[0] != null) { item0.setAttribute('data-currentidx', newIndexVals[0]) }; if (newIndexVals[1] != null) { item1.setAttribute('data-currentidx', newIndexVals[1]) }; if (newIndexVals[2] != null) { item2.setAttribute('data-currentidx', newIndexVals[2]) }; if (newIndexVals[3] != null) { item3.setAttribute('data-currentidx', newIndexVals[3]) }; };</script> |
End-result of script editors in SharePoint Online
Separate authoring site collection
Final Thoughts
I hope this post helped illustrate ways to display content across traditional SharePoint boundaries without cross-site publishing and how to template those displays for reuse. SharePoint Online might eventually get cross-site publishing feature, but that doesn’t mean you have to wait to achieve the same result. In fact, the script approach is so similar to display templates, it should be an easy transition to cross-site publishing in the future. I want to give a shout out to my colleague Nathan Miller for has assist in this vision.
Comments
Anonymous
April 12, 2013
Great article. www.sharepoint-journey.comAnonymous
April 30, 2013
It appears that you used the "Publishing Portal" template to create DallasMTCAuth. What type of site template did you use for DallasMTC?Anonymous
May 03, 2013
Hey Matt...I used the "Publishing Portal" template for both site collections. The DallasMTC site uses a custom design package so it isn't quite as obviousAnonymous
May 12, 2013
The problem I see here that it's ok for displaying the news snippets but won't you end up on the other site collection if someone clicks on the link?Anonymous
May 14, 2013
Hey Stefan...you would likely complement these rollups with a detail page that can read an ID from the url and display content accordingly. Again, this is similar to cross-site publishing where a catalog connection automatically creates a detail page that is driven from url tokens.Anonymous
June 22, 2013
Great article - proving useful Thanks!Anonymous
July 17, 2013
The comment has been removedAnonymous
July 18, 2013
Ben - OOTB, a publishing site will scope search to content within that site collection...which should be nothing in the new cross-site publishing model (or very little). The way to make OOTB search work in the publishing site is to either a) configure it to use a search center or b) create a result source for the publishing site that includes the catalogs from the authoring site. For www.dallasmtc.com I used the result source method, where my result source was scoped to 5 specific catalogs in my authoring site (news, media, contributors, events, and partners). If you are interested, email me and I'd be happy to send you the result source query. Thanks!Anonymous
September 13, 2013
What is with the user permissions? When I use it on the public facing site how people can see the news? Give the Rest API the news when the internet site visitor not a signed in user? Thanks StefanAnonymous
February 17, 2014
When I try to implement I get "Uncaught ReferenceError: $ is not defined" Has somthing change in Sharepoint 2013 Online that prevents this from working with the current version of Sharepoint 2013 Online?Anonymous
February 23, 2014
Lookup Plus for SharePoint 2013, It is more than Sharepoint Lookup. Cascaded Lookup, Filtered lookup, Cross-site Lookup /drop down/ and some controls are free. ("Create new item" link) Visit, http://www.azu.mn Or watch the channel www.youtube.com/watchAnonymous
June 17, 2014
The comment has been removedAnonymous
April 13, 2015
The comment has been removed