jquery

A 2-post collection

Sidebar Mechanics of a Custom Bootstrap Site

Written by Michael Earls
 bootstrap  ghost  jquery  html

Ghost development makes it easy to define a custom sidebar by using partials. However, the partial runs in the context of the post.hbs or page.hbs files. This limits the functionality that you can provide.

The sidebar partial hierarchy

For my custom Bootstrap theme, I created two separate partial files for the sidebar; one for the author page, and one for the page, post, and tag templates. I did this so that I could display information about myself on those pages. This, of course, assumes that you are running a single author blog. It would be a bit more complicated for multiple authors (if you even wanted to do such a thing at all).

Here is the code for the post page page which shows the location of the sidebar as a bootstrap <div /> with the id of "sidebar-container" and a css class of col-md-4.

<div class="row">
    <div id="sidebar-container" class="col-md-4">
        {{> sidebar author}}
    </div> <!-- .col-md-4 -->
    <div class="col-md-8">
        <!-- content... -->
    </div> <!-- .col-md-8 -->
</div>

Notice that I sent in the author context so that the sidebar would have access to the author data.

Here is the author.hbs partial that is used in the above code:

<!-- sidebar -->
{{!include the various components}}
{{> "search-form"}}
<div id="sidebar-component-container"></div>
{{> "bio-panel"}}
{{> "sidebar-theme-picker"}}
<!-- /sidebar -->

This sidebar adds a sidebar component container (which I'll discuss below) as well as calls to bio-panel and sidebar-theme-picker partials.

Here's the bio-panel.hbs which takes advantage of the fact that we passed in the author context from the page:

<a name="author" class="sr-only"></a>
<div class="panel panel-primary">
    <div class="panel-heading">
        About {{name}}
    </div>
    <div class="panel-body">
        {{#if image}}
        <img class="img-thumbnail center-block" src="{{image}}" alt="{{name}}'s Picture" />
        {{/if}}
        <h1 class="author-title center-block">{{name}}</h1>
        {{#if bio}}
            <div class="bio-container">
                <h2 class="author-bio text-justify">{{bio}}</h2>
            </div>
        {{/if}}
        <div class="author-meta center-block sidebar-meta">
            {{#if location}}<p><i class="fa fa-map-marker"></i> <small>{{location}} <a href="https://www.google.com/maps/place/{{ encode location }}" aria-label="{{location}} map link" target="locationMap">(map)</a></small></p>{{/if}}
            {{#if website}}<p><i class="fa fa-link"></i> <small><a href="{{website}}">{{website}}</a></small></p>{{/if}}
        </div>
    </div>
</div>

The <a /> with the sr-only class is simply a bookmark for screen readers. It is not displayed to the user.

All of the data that is displayed is coming from the author context that we passed into the original partial.

This works well when we only need to use the information from a single context like page or post where we have author information, but what happens when we want to display the author information from a context that has multiple possible authors? In this case, we pass in the author context like so (this is from the tag.hbs page):

<div id="sidebar-container" class="col-md-4">
    {{#posts.[0].author}}
    {{> "sidebar"}}
    {{/posts.[0].author}}
</div> <!-- .col-md-4 -->

In this case, I am simply pulling the author context off of the first post. Obviously, if the author you want to display in the sidebar is not the author of the first post, you'll need to pick a different post.

Note: there are (as of this writing) experimental API calls you can make to retrieve some of this information, but it outputs some code in the html header that I am not comfortable with. I just get a weird feeling doing it that way.

Sleight-of-hand tricks for sidebar content out of context

If you are reading this post and the Bootstrap theme is enabled, you should see a panel with Social Media share buttons in it. As you can see from the code above, those links are not included in any of the sidebar code.

For the post social media links to work, I have included a hidden <div /> in the post.hbs template that contains another <div /> decorated with the css class of sidebar-component.

<div class="panel panel-primary share sidebar-component">
    <div class="panel-heading">
        <p class="panel-title">Share this post</p>
    </div> <!-- .panel-heading (share) -->
    <div class="panel-body">
        <a class="text-primary share-link" href="https://twitter.com/intent/tweet?text={{encode title}}&amp;url={{url absolute="true"}}"
           onclick="window.open(this.href, 'twitter-share', 'width=550,height=235');return false;"
           title="Share on Twitter" aria-label="Share on Twitter"><i class="fa fa-3x fa-twitter-square"></i><span class="sr-only">Twitter</span></a>
        <a class="text-primary share-link" href="https://www.facebook.com/sharer/sharer.php?u={{url absolute="true"}}"
           onclick="window.open(this.href, 'facebook-share','width=580,height=296');return false;"
           title="Share on Facebook" aria-label="Share on Facebook"><i class="fa fa-3x fa-facebook-square"></i><span class="sr-only">Facebook</span></a>
        <a class="text-primary share-link" href="https://plus.google.com/share?url={{url absolute="true"}}"
           onclick="window.open(this.href, 'google-plus-share', 'width=490,height=530');return false;"
           title="Share on Google+" aria-label="Share on Google+"><i class="fa fa-3x fa-google-plus-square"></i><span class="sr-only">Google+</span></a>
    </div> <!-- .panel-body (share) -->
</div> <!-- .panel (share) -->

Then, there is code in the page initialization that moves any of those divs to the <div /> in the sidebar with the id of sidebar-component-container.

Here is that code:

function moveSidebarItems() {
    // move items to the sidebar
    $('.sidebar-component').each(function () {
        $(this).detach().appendTo($('#sidebar-component-container'));
    });
}

$(moveSidebarItems);

This is pretty self-explanatory. It uses jQuery to move the intended sidebar content to the sidebar after the page has loaded. That means that the server has already processed it and used the context that the div is defined under. In simple terms, it means that you can hide a div in any part of a page or in any partial under any context and display data about that context, then simply make sure that the div has the class sidebar-component attached to it and it will get moved into place automatically. It should be noted that there is no defined order as that is out of the scope of this discussion. It could be done with custom html attributes (data-sidebar-order or similar attribute and ordered during the move at page ready).

Another approach to adding icons to dynamically generated links

Written by Michael Earls
 bootstrap  programming  icons  jquery  javascript

In my previous post, I outlined the code necessary to add icons to navigation links. I have since updated my implementation to use a function to add the link-to-icon mapping.

I have refactored my theme code to use the new approach.

The GitHub repository is ghost-cerkit-theme.

Instead of using JSON to define the link-to-icon mapping (as outlined in the previous post), we use an object on the window object:

window.linkIconMap = {};

Then, we define a function that we can use to add a new mapping:

window.addLinkIcon = function (target, icon, size) {  
    // check to see if we have maps defined already
    if (!window.linkIconMap.maps) {
        // if not, define it
        window.linkIconMap.maps = [];
    }

    // if we have a size passed in, use it, otherwise use the default icon size on our icon map. If that's missing, use nothing (Font Awesome default size)
    var iconSize = size ? size : 'defaultIconSize' in window.linkIconMap ? window.linkIconMap.defaultIconSize : '';
    window.linkIconMap.maps.push({ "target": target, "icon": icon, "size": iconSize });
};

First, we see if our maps array exists. If not, we create it. This prevents us from doing anything if the theme user never added any mappings.

After we check on the maps, we figure out what size to use. Basically, the same rules apply as before: if the map does not define a size, then we use the default size stored in defaultIconSize on our window.linkIconMap (but only if it's defined).

// sample definition for icon size
window.linkIconMap.defaultIconSize = 'fa-lg';

As a last resort, we fall back on an empty string, which has the effect of using the default size from the Font Awesome font.

In order for the theme user to create the map, they simply call the window.addLinkIcon() function:

// Navbar Icon Map  
window.addLinkIcon('nav-home', 'fa-home');  
window.addLinkIcon('nav-about', 'fa-user');  
window.addLinkIcon('nav-my-public-key', 'fa-key');  
window.addLinkIcon('nav-test', 'fa-cogs', 'fa-2x' /* optional */);

Note: you may notice that it's possible to bind an icon to any class on your page, not just navigation links. For example, I just set up my navbar expansion button (displayed when the screen is too narrow to show all the buttons) so that it does not have anything in it. I then add a call to window.addLinkIcon('navbar-toggle', 'fa-sitemap', 'fa-2x'); for it. This will then apply to all navbar toggles.

Here is where the toggle button is defined.

When the page loads, it calls the following code:

function bindLinkIcons() {  
    if (window.linkIconMap.maps) {
        var curIconMap;
        var curSize;

        for (var i = 0; i < window.linkIconMap.maps.length; i++) {
            // get a handle on the current icon map
            curIconMap = linkIconMap.maps[i];

            // set the icon on the navbar item
            createIcon(curIconMap.target, curIconMap.icon, curIconMap.size);
        }
    } else {
        console.warn('cerkit-bootstrap theme supports navbar link icons. Add the following to your footer in code injection: \<script\>window.addLinkIcon(/* target = */ "nav-home", /* icon = */ "fa-home", /* (optional) size = */ "fa-3x");\</script\>');
    }
}

$(bindLinkIcons);

We're only going to bind the icons if we have a map array to work with. If we do, then we simply loop through each entry and call the createIcon() function, passing in the relevant information.

Here is the createIcon() function definition:

function createIcon(target, icon, size) {  
    var iconElement = $(document.createElement('i')).attr('class', 'link-icon fa fa-fw ' + icon + ' ' + size).append('&nbsp;');
    var targetNavbarItem = $('.' + target);
    var targetItemFirstChild = $(targetNavbarItem).children()[0];

    // figure out if the nav item has any links in it. If so, use that as the icon parent.
    // Otherwise, use the navbarIconItem.
    var iconParentElement = targetItemFirstChild == null ? targetNavbarItem : targetItemFirstChild;

    // insert the icon element at the beginning of the parent
    $(iconParentElement).prepend(iconElement);
}

The first thing we do is create a new i tag to use as the icon container element. We then add the appropriate class attributes based on the icon and size values passed into the function. After the icon is defined, we append a non breaking space so that the icon isn't too close to the link. We then add the link-icon class to each of the icons that gets bound to a link. Then, in the site's css file, we simply add the following code:

.link-icon {  
    margin-right: 3px;
}

The next thing that happens is the code gets a handle on the target by selecting it based on the class name passed into the function (as the target argument).

Then, the first child element is selected (it is assumed that this element will either be a a tag or plain text. Whatever it is, we'll prepend our new i element representing our icon.

I believe that this approach is superior to the original approach as it is friendlier and more expressive to call a function to add a mapping rather than some arbitrary (and potentially confusing) JSON code.

I have created a Gist file that contains the core code to make this work.