Custom site header and footer using a SharePoint-hosted add-in

In this post, I will show you how to create a custom site header and footer for SharePoint Online/Office 365 that will render on all pages using a SharePoint-hosted add-in. This could be used to post critical alerts or to specify the level of business impact of the content stored within a particular site (e.g., high, medium, or low).

This add-in will accomplish the following, all without a single line of server-side code:

  • Upon installation, deploy 3 JavaScript files to the Style Library of the host web
  • Register 3 user custom actions in the host web to ensure these JavaScript files are loaded on every page
  • Via an add-in part, provide a mechanism for users to enable/disable and set properties of the custom header and footer (e.g., message text, background/text color)
  • Store and retrieve configuration parameters for the site header and footer in the host web’s property bag

Rendering a custom header and footer

We might be tempted to edit the master page directly to add a header and a footer, but this is not considered a best practice (especially in SharePoint Online, where master page changes can be rolled out quite frequently). Instead, our add-in will make use of JavaScript techniques to insert <div> elements at the top and bottom of the page for our header and footer:

  • In SharePoint Online/Office 365, our header <div> (with ID customHeader) will be inserted above the <div> with ID suiteBarTop (for SharePoint 2013 on-premises, you’ll need to update the script to reference ID suiteBar instead).
  • Our footer <div> (with ID customFooter) will be inserted below the <div> with ID s4-bodyContainer.
  • NOTE: The <div> IDs in SharePoint Online are never set in stone and could change at any time. If they do, you will need to update HeaderFooter.js to reference the new <div> IDs.

jQuery makes it easy to create our custom header and footer <div> elements and insert them in the appropriate location within the DOM with its .insertBefore() and .insertAfter() functions following this approach:

$("<div id='customHeader' class='ms-dialogHidden'>HEADER TEXT</div>").insertBefore("#suiteBarTop");
$("<div id='customFooter' class='ms-dialogHidden'>FOOTER TEXT</div>").insertAfter("#s4-bodyContainer");

Making the footer “sticky”

Huge shout-out to Randy Drisgill for his SharePoint 2013 Sticky Footer implementation. The “sticky” footer anchors the footer <div> to the bottom of the page (or the bottom of the browser window if the available screen real estate exceeds the amount of content on a given page). I only needed to make one change to Randy’s StickyFooter.js implementation, and that was to account for the height offset imposed by the addition of our customHeader <div>:

var difference = windowheight - (bodyheight + ribbonheight + footerheight + $("#customHeader").outerHeight());

Provisioning files to the host web via JavaScript

For anyone following the OfficeDev PnP, the concept of remote provisioning to allow add-ins to deploy files to the host web should be a familiar one. However, the PnP examples make use of the .NET Managed CSOM to do this, which is a perfectly valid technique but would require us to develop a provider-hosted add-in (allowing that code to run in Azure or some other web server). Since I wanted to create a SharePoint-hosted add-in, I had to find a way to accomplish this using only JavaScript. Thankfully, I found Chris O’Brien’s post with code showing how to provision files to the host web using JavaScript. You will see that my code is based heavily on the example he provides and provisions the following files from the add-in web to the host web:

  • jQuery-1.9.1.min.js – jQuery
  • HeaderFooter.js – our logic to read the header/footer configuration data from the host web property bag and render the header <div> and footer <div> elements
  • StickyFooter.js – Randy Drisgill’s Sticky Footer implementation for SharePoint 2013 (with the one tweak described above)

Add-in part for setting configuration values

The add-in also includes an add-in part (deployed to and rendered from the add-in web) that allows users to enable/disable the header/footer and set the text and colors:

addinpartWhen the add-in part loads, it uses JavaScript to query the property bag of the host web to see if these settings already exist and if so, prepopulates the values in the form. Once the user has made the necessary changes, clicking Set Values will save the changes back to the property bag (again, using JavaScript).

NOTE: The need for our add-in to read and write data to and from the host web property bag requires us to request the FullControl permission at the Web scope in our add-in manifest (AppManifest.xml):

<AppPermissionRequests>
  <AppPermissionRequest Scope="http://sharepoint/content/sitecollection/web" Right="FullControl" />
</AppPermissionRequests>

What about MDS?

SharePoint 2013 introduced the Minimal Download Strategy (MDS), which reduces page load time by sending only the differences when users navigate to a new page. While this is a wonderful benefit, it wreaks havoc with solutions that need to manipulate the DOM every time a new page is loaded. In other words, our header and footer may render perfectly on the home page when we first load it, but thanks to MDS when we navigate to the “Documents” library, only part of the page will actually be re-rendered. Our header and footer will not display properly (if at all) when only part of the page is re-rendered to support a new page request.

For much deeper reading on this subject, I encourage you to go through Wictor Wilen’s blog posts introducing the MDS and explaining the correct way to execute JavaScript functions on MDS-enabled sites. My code is based on Wictor’s solution and works properly in scenarios where MDS is enabled and where it is not (e.g., on Publishing sites or any site where the MDS feature has been deactivated).

We handle MDS by calling RegisterModuleInit() with the function we need to execute on every page load in HeaderFooter.js:

if (typeof _spPageContextInfo != "undefined" && _spPageContextInfo != null) {
    // MDS enabled
    RegisterModuleInit(_spPageContextInfo.siteServerRelativeUrl + 'Style Library/Headerfooter.js', DJ.HeaderFooter.Render.initializeHeaderFooter);
}
// Run now on this page (and non-MDS scenarios)
DJ.HeaderFooter.Render.initializeHeaderFooter();

The code

I have posted the code for this add-in to GitHub at the following location:

https://github.com/dannyjessee/SiteHeaderFooter

I encourage you to download it, try it out in your environment, and let me know if you run into any issues with it. My sincere thanks go out to Chris O’Brien, Randy Drisgill, and Wictor Wilen for giving me the building blocks needed to put this add-in together.

  • Kees

    Hello,
    I think this is just the framework I am looking for, I just would like to know two things:

    1. I would like to include some html into the header and footer. The footer is relatively static and will reside in a separate file, but the header contains a custom javascript menu (which I would includeAfter the suiteBarTop). How easy would it be to include that in this app? I have had a look and think I can replace the header/footer text with an iframe to accomplish that, however have not been able to test it because of the next issue.

    2. I am relatively new to sharepoint and can’t work out how to use this add-in. I suspect I need to upload it somewhere, but can’t work out where or how.

    Thank you for your help.

    • Kees,

      You can include HTML in the header and footer without having to use an iframe. If you look at HeaderFooter.js, you’ll see that I’m using jQuery to inject HTML into the DOM. You can follow this pattern to load anything you would like, formatted any way you want.

      To answer your second question, the code I posted is a SharePoint-hosted add-in project for Visual Studio. You can deploy it to a local SharePoint 2013 development server or to an Office 365 developer tenant site in SharePoint Online. I would strongly encourage you to review the documentation Microsoft has put together at dev.office.com to learn more about this paradigm and to help you get started.

      Best of luck!

      • Kees

        OK, lots of reading and installing and learning Visual Studio later, I have managed to create and App Catalog (there does not seem to be an Add-in option in my admin center). I have managed to upload the add-in to the App Catalog and install it in my test site. However I cannot work out how to get to the configuration settings. Can you help me on that one please.

        Thanks.

        • The configuration settings are accessed via in an add-in part. After deploying the add-in, if you edit a page in the host web and choose “Add a Web Part,” you’ll see an entry under “Apps” called “Set Custom Header/Footer.” You can add or remove this from any page at any time as needed to set and update them.

          • Kees

            Sorry for keeping on at this. Your replies are generally very helpful, but I keep getting stuck at different places. I have now found the configuration settings and set them, but the header and footer are still not appearing.

            Here is what I have done:
            * Unzip the downloaded file
            * Open the project in Visual Studio (Community 2015 with Office add-in stuff added)
            * Ignore the error about the Site URL being empty
            * Build -> Publish… -> Package the add-in
            * upload the SiteHeaderFooter.app to the App Catalog
            * add the SiteHeaderFooter app to my test site (which is essentially empty) (uses the Oslo template).
            * Create and edit a new site page
            * Insert -> Web Part -> Apps -> Set Custom Header/Footer
            * Save the page
            * Set the header text and colours
            * Click ‘Set Values’ (green text appears to say it is successful)
            * Reload the page (also tried Shift + Reload)

            No header and footer are appearing, I inspected the rendered page and I can see the suiteBarTop div, but there is not custom header div before it. I also don’t seem to get any errors in the console. Have you got any ideas what I am doing wrong. If not, how would I go about debugging this?

          • Have you launched the add-in yet (i.e., from the Site Contents screen)? Typically you would deploy the add-in package directly from Visual Studio to your Office 365 Developer Tenant site before deploying to a production app catalog. This would launch the add-in’s start page (which is responsible for registering the scripts that inject the custom HTML into each page), but you can also navigate directly to the add-in to accomplish this.

          • Kees

            Hmmm, can’t see a way to launch the add-in. When I click on the ‘…’ next to the add-in I get a small menu (see first screenshot below) and when I normally click on it I get a nearly empty web page (see the second picture), but I can’t do anything there. Will try to work out how to do this directly from Visual Studio tomorrow, am not at my desk today.

          • Kees

            Just realise that in the empty page mentioned above I get the following error in my console. Not sure if that is causing the problem, but I thought I mention it anyway.

            06:54:30.421 TypeError: SP.ClientContext is not a constructor
            DJ.AddInInstall.HostWebSetup</init() App.js:93
            DJ.AddInInstall.HostWebSetup</<.execute() App.js:99
            App.js:111
            b.Callbacks/c() jquery-1.9.1.min.js:22
            b.Callbacks/p.fireWith() jquery-1.9.1.min.js:22
            .ready() jquery-1.9.1.min.js:22
            H() jquery-1.9.1.min.js:22
            1 App.js:93:25

          • Kees

            OK, got it to work. Have the change the last three lines of App.js from:

            $(document).ready(function () {
            DJ.AddInInstall.HostWebSetup.execute();
            });

            to:

            $(document).ready(function () {
            ExecuteOrDelayUntilScriptLoaded(DJ.AddInInstall.HostWebSetup.execute, “sp.js”);
            });

            and that seems to have made the difference.

            Thank you so much for making this available.

          • Great catch! Thank you for noticing that. I had just been getting lucky and sp.js was already loaded every time I had run it in the past, but after you posted this, I noticed it had started happening in my environment as well. I have updated the code on Github to reflect this change.

          • Kees

            Another small error, that hides the fact that when you deploy a new version the JavaScript files are not deployed properly to “Site Assets”:

            onProvisionFileFail = function () {

            should be:

            onProvisionFileFail = function (sender, args) {

            It then starts warning you that the three JavaScript files it tries to deploy to ‘Site Assets’ should be checked out. I have not had time yet to look into this, but as I haven’t changed those files since the first deploy it is not yet causing me problems.

          • Another good catch. Thanks!

      • Kees

        Very happy to have this working for my test site now. Just one more question (I hope). How easy would it be to change the code to add the header and footer to this site and all its sub-sites (and sub-sub-sites, etc).

        • You have a couple options here. My intent was to allow these properties to be controlled at the site level, so you could deploy the add-in on any site where you wanted it to appear without necessarily having it appear on ALL subsites. Alternately, you could use the JSOM to iterate through all sites and subsites when the add-in is deployed, but the individual header/footer properties would still be controlled at the individual site level. You may also want to look into a technique called app/add-in stapling (disclaimer: I have not tried this), which would ensure the add-in gets deployed to any new subsites that get created in the future: http://blogs.msdn.com/b/richard_dizeregas_blog/archive/2013/03/04/sharepoint-2013-app-deployment-through-quot-app-stapling-quot.aspx

          • Kees

            Looks like stapling is not available for Sharepoint Online.

            Still working on getting this working, have now changed activateHeaderFooter as follows, but keep getting an error message that just gives me a correlation ID, and as I don’t seem to be able to get to the ULS logs on SPO that is not much help to me. Any change you can easily spot what I am doing wrong here?

            activateHeaderFooter = function () {
            var level = 0;
            var activateHeaderFooterInner = function (web) {
            level++;
            var ctx = web.get_context();
            var webs = web.get_webs();
            ctx.load(webs);
            ctx.executeQueryAsync(
            function () {
            var webCustomActions = web.get_userCustomActions();

            activateUserCustomAction(webCustomActions, 10023, ‘jquery-1.9.1.min.js’);
            activateUserCustomAction(webCustomActions, 10024, ‘HeaderFooter.js’);
            activateUserCustomAction(webCustomActions, 10025, ‘StickyFooter.js’);

            ctx.load(webCustomActions);

            for (var i = 0; i < webs.get_count() ; i++) {
            activateHeaderFooterInner(webs.getIntemAtIndex(i));
            }
            level–;
            if (level == 0) {
            ctx.executeQueryAsync(onActivateSuccess, onActivateError);
            }
            },
            function (sender, args) {
            $('#activateOutput').append('Something went wrong at level ‘ + level + “: n” + args.get_message() + ‘‘);
            }
            );
            };

            var clientContext = new SP.ClientContext(hostWebUrl);
            var factory = new SP.ProxyWebRequestExecutorFactory(hostWebUrl);
            clientContext.set_webRequestExecutorFactory(factory);

            var hostWeb = clientContext.get_web();
            activateHeaderFooterInner(hostWeb);
            },

            If I get the appWeb context I don’t get an error, but the header only appears in the top level site.
            The error message I am getting in the console is like this:

            Mon Jan 11 2016 11:27:14 GMT+0000 (GMT):The frame element does not exist. Put request in queue jquery-1.9.1.min.js line 22 > eval:2:23587
            “Mon Jan 11 2016 11:27:14 GMT+0000 (GMT):Created IFrame https://[mysite].sharepoint.com/sites/testing/_layouts/15/AppWebProxy.aspx?SP.AppPageUrl=https://%5Bmysite%5D-2473d327d53a3f.sharepoint.com/sites/testing/SiteHeaderFooter/Pages/Default.aspx” jquery-1.9.1.min.js line 22 > eval:2:23587
            Mon Jan 11 2016 11:27:14 GMT+0000 (GMT):Create IFrameLoadTimeout 28 jquery-1.9.1.min.js line 22 > eval:2:23587
            Mon Jan 11 2016 11:27:15 GMT+0000 (GMT):Processing IFRAME onload event jquery-1.9.1.min.js line 22 > eval:2:23587
            Mon Jan 11 2016 11:27:15 GMT+0000 (GMT):Clear IFrameLoadTimeout 28 jquery-1.9.1.min.js line 22 > eval:2:23587
            “Mon Jan 11 2016 11:27:15 GMT+0000 (GMT):Start to ping the IFRAME https://[mysite].sharepoint.com/sites/testing/_layouts/15/AppWebProxy.aspx” jquery-1.9.1.min.js line 22 > eval:2:23587
            Mon Jan 11 2016 11:27:15 GMT+0000 (GMT):Create IFramePingTimeout 41 jquery-1.9.1.min.js line 22 > eval:2:23587
            Mon Jan 11 2016 11:27:15 GMT+0000 (GMT):RequestExecutor.PostMessage.Message: {“command”:”Ping”,”postMessageId”:”SP.RequestExecutor2″} jquery-1.9.1.min.js line 22 > eval:2:23587
            “Mon Jan 11 2016 11:27:15 GMT+0000 (GMT):RequestExecutor.PostMessage.Target: https://[mysite].sharepoint.com/sites/testing/_layouts/15/AppWebProxy.aspx” jquery-1.9.1.min.js line 22 > eval:2:23587
            ErrorPage.OnMessage: Origin=https://[mysite]-2473d327d53a3f.sharepoint.com, Data={“command”:”Ping”,”postMessageId”:”SP.RequestExecutor2″} AppWebProxy.aspx:187:6
            ErrorPage.PostMessage: Origin=https://[mysite]-2473d327d53a3f.sharepoint.com, Data={“command”:”Ping”,”postMessageId”:”SP.RequestExecutor2″,”responseAvailable”:false,”errorCode”:-1007,”errorMessage”:”Correlation ID: d729549d-70e4-2000-2142-365ed1cc03bc”} AppWebProxy.aspx:217:8
            Mon Jan 11 2016 11:27:15 GMT+0000 (GMT):RequestExecutor.OnMessage jquery-1.9.1.min.js line 22 > eval:2:23587
            Mon Jan 11 2016 11:27:15 GMT+0000 (GMT):RequestExecutor.OnMessage: Message.data={“command”:”Ping”,”postMessageId”:”SP.RequestExecutor2″,”responseAvailable”:false,”errorCode”:-1007,”errorMessage”:”Correlation ID: d729549d-70e4-2000-2142-365ed1cc03bc”} jquery-1.9.1.min.js line 22 > eval:2:23587
            Mon Jan 11 2016 11:27:15 GMT+0000 (GMT):RequestExecutor.OnMessage: Message.origin=https://[mysite].sharepoint.com jquery-1.9.1.min.js line 22 > eval:2:23587
            “Mon Jan 11 2016 11:27:15 GMT+0000 (GMT):Successfully load frame for https://[mysite].sharepoint.com/sites/testing/_layouts/15/AppWebProxy.aspx” jquery-1.9.1.min.js line 22 > eval:2:23587
            Mon Jan 11 2016 11:27:15 GMT+0000 (GMT):RequestExecutor.PostMessage.Message: {“command”:”Query”,”url”:”https://[mysite].sharepoint.com/sites/testing/_api/contextinfo”,”method”:”POST”,”body”:null,”headers”:{“ACCEPT”:”application/json;odata=verbose”},”postMessageId”:”SP.RequestExecutor1″,”timeout”:180000} jquery-1.9.1.min.js line 22 > eval:2:23587
            “Mon Jan 11 2016 11:27:15 GMT+0000 (GMT):RequestExecutor.PostMessage.Target: https://[mysite].sharepoint.com/sites/testing/_layouts/15/AppWebProxy.aspx” jquery-1.9.1.min.js line 22 > eval:2:23587
            ErrorPage.OnMessage: Origin=https://[mysite]-2473d327d53a3f.sharepoint.com, Data={“command”:”Query”,”url”:”https://[mysite].sharepoint.com/sites/testing/_api/contextinfo”,”method”:”POST”,”body”:null,”headers”:{“ACCEPT”:”application/json;odata=verbose”},”postMessageId”:”SP.RequestExecutor1″,”timeout”:180000} AppWebProxy.aspx:187:6
            ErrorPage.PostMessage: Origin=https://[mysite]-2473d327d53a3f.sharepoint.com, Data={“command”:”Query”,”postMessageId”:”SP.RequestExecutor1″,”responseAvailable”:false,”errorCode”:-1007,”errorMessage”:”Correlation ID: d729549d-70e4-2000-2142-365ed1cc03bc”} AppWebProxy.aspx:217:8
            Mon Jan 11 2016 11:27:15 GMT+0000 (GMT):RequestExecutor.OnMessage jquery-1.9.1.min.js line 22 > eval:2:23587
            Mon Jan 11 2016 11:27:15 GMT+0000 (GMT):RequestExecutor.OnMessage: Message.data={“command”:”Query”,”postMessageId”:”SP.RequestExecutor1″,”responseAvailable”:false,”errorCode”:-1007,”errorMessage”:”Correlation ID: d729549d-70e4-2000-2142-365ed1cc03bc”} jquery-1.9.1.min.js line 22 > eval:2:23587
            Mon Jan 11 2016 11:27:15 GMT+0000 (GMT):RequestExecutor.OnMessage: Message.origin=https://[mysite].sharepoint.com

          • It looks like you have a typo in the call to getItemAtIndex (what you’ve pasted above says getIntemAtIndex).