Organize your Live Blog using tags

Using Live Center you can divide your live blog into different tabs based on their tags. This allows you to categorize posts and provide your readers with a way to quickly find posts they are interested in. 

As an example, you can create a continuous News Feed with the tabs News, Sports, and Weather. When a user clicks a tab, the live blog will filter the whole feed to show only posts relating to that topic. 

A live example of a Live Blog which uses these features can be found here in a Football Transfer Window Live Blog created by Nettavisen.no

This can be achieved using out of the box functionality in Live Center. Below is a short guide to help you set up such a feed.

Introduction

Live Center relies on tags to categorize posts into one or more tabs. Posts are easily tagged when they are created and therefore are easily added by your content creators when they publish posts.

In addition to categorizing posts into tabs, tags can also be used by users to see only posts with that specific tag.

For example, as you can see in the screenshots below we have added 3 tags: “ Tab1”, “Vålerenga” and “Real Madrid”. Tab1 is the tag which we use to categorize posts into a tab, while Vålerenga and Real Madrid are shown to readers and is available to click to see only posts with a specifc tag.

Because “Tab 1” is the name of the tab, we hide this tag from tags below the content. We will provide more details about how to do this in the following steps. 

Useful resources before you start

Introduciton to how you can add Live Center can be found here: https://livecentersupport.norkon.net/article/23-implementation

Throughout this guide, we will use some methods from NcPost and NcCore namespaces that are introduced in more detail here: https://livecentersupport.norkon.net/category/21-technical

1. Create containers for tabs and highlighted tags in .html file

  <div class="lc-feed-container">
        <div>
            <div id="tab-panel"></div>
            <div id="tag-panel"></div>
        </div>
....
    </div>

2. Use NcPosts.hookOptionsAfterDefaults to customize the feed included header, content, comments, and tags with channel “options” object

NcPosts.hookOptionsAfterDefaults(options => {})

3. Getting the feed control object to faciliate changing tags filter after the fact

options.started = NcPosts.appendFunction(options.started, (control: NcPosts.StartResult) => {
        FeedWithTabs.feedControl = control;
    });

4. Set the default initial tab

options.selectedTag = FeedWithTabs.defaultTab.name;

5. Hook to the tabs panel to inject them

 FeedWithTabs.hookTabs();

6. Use hookTabs() to allow us to create a tabs panel using NcHtml. 

ToggleActiveClass() toggle classes according to selected tab. With the function selectTag() we use to filter posts by selected tag/tab

namespace FeedWithTabs {

    export var feedControl: NcPosts.StartResult;
    export var selectedTag: string = null;
    export const logoUrl = "https://staticcdn.norkon.net/nettavisen/sillyseason/logos/";

    export const tabs = [
        {
            name: "Tab 1", element: null
        },
        {
            name: "Tab 2", element: null
        },
        {
            name: "Tab 3", element: null
        }
    ]

    export const defaultTab = tabs[0];

    export function selectTag(tag: string) {

    const teamPanel = document.getElementById("tag-panel");

        selectedTag = tag;

        if (feedControl) {
            feedControl.selectTag(selectedTag);

            if (selectedTag === defaultTab.name) {
                window.history.pushState({}, document.title, "?");
                toggleActiveClass(tabs[0].element);
            } else {
                window.history.pushState({}, document.title, UrlHandling.createTagUrlSearch(selectedTag));
                toggleActiveClass(null);
            }

            window.scrollTo && window.scrollTo(0, 0);
        }

        for (let i = 0; i < tabs.length; i++) {

            if (selectedTag === tabs[i].name) {
                teamPanel.style.display = "none";
                toggleActiveClass(tabs[i].element);
                return;
            } 

        };

        const teamTag = teamTags[selectedTag];

        teamPanel.style.display = "";

        NcHtml.clearAndAppend(teamPanel, "div.selected-tag-container", {
            _children: [
                {
                    _tag: "div.selected-tag",
                    style: teamTag ? "" : "margin-left: 10px; padding: 10px 0;",
                    _child: {
                        _tag: "img",
                        style: teamTag ? "display:" : "display:none",
                        src: teamTag ? logoUrl + teamTag.logo : "",
                    },
                    _text: tag,
                },
                {
                    _tag: "div.unselect-tag",
                    style: "fill: #fff",
                    _onclick: e => selectTag(defaultTab.name),
                    _html: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm5 15.538l-3.592-3.548 3.546-3.587-1.416-1.403-3.545 3.589-3.588-3.543-1.405 1.405 3.593 3.552-3.547 3.592 1.405 1.405 3.555-3.596 3.591 3.55 1.403-1.416z"/></svg>',
                }
            ]
        });
    }

    export function hookTabs() {

        const tabsPanel = document.getElementById("tab-panel");

        NcHtml.append(tabsPanel, "div.tabs-container", {
            _children: tabs.map(tab =>
                ({
                    _tag: "button",
                    _func: e => tab.element = e,
                    _onclick: function () {
                        selectTag(tab.name);
                        toggleActiveClass(tab.element);
                    },
                    _text: tab.name,
                }),
            )
        }
        );
    }

    export function toggleActiveClass(element) {

        for (let tab of tabs) {
            let currentTab = tab.element;
            if (currentTab === element) {
                currentTab.classList.add("active-tab")
            } else {
                currentTab.classList.remove("active-tab")
            }
        }
    }
}

7. Next we check if we have different initial tag selected

 const requestedTag = FeedWithTabs.UrlHandling.getSelecedTagFromUrl();

    if (requestedTag) {
        options.selectedTag = requestedTag;
        FeedWithTabs.selectTag(requestedTag);
    } else {
        FeedWithTabs.selectTag(FeedWithTabs.defaultTab.name);
    }

getSelecedTagFromUrl() enable us to check specific tag highlighted.

public static getSelecedTagFromUrl(): string | null {
            try {
                const str = UrlHandling.getUrlVars().t;

                //Format: ?t=lc-{tag}

                if (str && str.substring(0, 3) === "lc-" && str.substring(3)) {
                    return decodeURI(str.substring(3));
                }
            } catch (e) {

            }

            return null;
        }

private static getUrlVars(): any {
            var params = {};

            if (location.search) {
                var parts = location.search.substring(1).split('&');

                for (var i = 0; i < parts.length; i++) {
                    var nv = parts[i].split('=');
                    if (!nv[0]) continue;
                    params[nv[0]] = nv[1] || true;
                }
            }
            return params;
        }

8. Check if there is a post to be highlighted

const requestedPostId = FeedWithTabs.UrlHandling.getLinkedPostIdFromUrl();

    if (requestedPostId) {
        options.linkedPostId = requestedPostId;

        options.postLinked = (postData, post, container) => {
            container.appendChild(NcHtml.create("button", {
                _onclick: () => {
                    window.history.pushState({}, document.title, "?")
                    container.parentElement.removeChild(container);
                },
                _text: "Lukk",
                class: "ncpost-linked-close"
            }));
        };
    }

getLinkedPostIdFromUrl()

public static getLinkedPostIdFromUrl(): number | null {

            try {

                const str = UrlHandling.getUrlVars().p;

                //Format: ?p=lc-{postId]-{channelId}

                if (str && str.substring(0, 3) === "lc-") {
                    const parsed = parseInt(str.split('-')[1]);

                    if (!isNaN(parsed)) {
                        return parsed;
                    }
                }
            } catch (e) {

            }

            return null;
        }

private static getUrlVars(): any {
            var params = {};

            if (location.search) {
                var parts = location.search.substring(1).split('&');

                for (var i = 0; i < parts.length; i++) {
                    var nv = parts[i].split('=');
                    if (!nv[0]) continue;
                    params[nv[0]] = nv[1] || true;
                }
            }
            return params;
        }

9. Replace the default tags renderer with your custom.

The default one doesn’t allow us to sort posts by tags. To use tabs also we need to create our own named ExampleTagsRenderer()

NcPosts.replaceInstanceOf(options.postRenderers, NcPosts.TagsAndQuantsRenderer,
     new FeedWithTabs.ExampleTagsRenderer());

10. Our own Tags Renderer: 

namespace FeedWithTabs {

    export class ExampleTagsRenderer implements NcPosts.PostRenderer {

        public shouldRun(postData: any): boolean {
            return (postData.tags && postData.tags.length);
        }

        public updateElement(state: any, postData: any, element: HTMLDivElement, container: HTMLDivElement) {

            // Checks for tag/quantorder.
            // Server saves the tags/quants in its own order. the editor might want them in another.
            postData.tags = (postData.content.tagOrder) ? postData.content.tagOrder : postData.tags;

            // returns empty tagquant element if there is no tags / quants at all.
            if ((!postData.tags || (postData.tags && !postData.tags.length))) {
                delete state.tagsElement;
                return null;
            }

            let innerElement = null;

            // checks if element parameter to function is created
            if (!element) {
                element = document.createElement("div");

                innerElement = state.innerElement = document.createElement("div");
                innerElement.setAttribute("class", "example-tags-container");
                element.appendChild(innerElement);

                const clearer = document.createElement("div");
                clearer.style.clear = "both";
                element.appendChild(clearer);
            } else {
                innerElement = state.innerElement;
            }

            if (NcPosts.tagquantArrayDiffers(state.prevTags, postData.tags)) {

                if (state.tagsElement) {
                    while (state.tagsElement.firstChild) {
                        state.tagsElement.removeChild(state.tagsElement.firstChild);
                    }

                    if (!postData.tags || (postData.tags && !postData.tags.length)) {
                        state.tagsElement.parentNode.removeChild(state.tagsElement);
                        state.tagsElement = null;
                    }
                } else {
                    state.tagsElement = document.createElement("div");
                    state.tagsElement.setAttribute("class", "example-tags");
                    innerElement.appendChild(state.tagsElement);
                }

                if (postData.tags && postData.tags.length) {
                    for (var tag of postData.tags) {

                        var tmpTxt = document.createTextNode(" " + tag + " "),
                            tmpEle = document.createElement("div");

                        const tagEle = document.createElement("span");

                        tagEle.appendChild(tmpTxt);

                        const selectedTag = tag;

                        const teamTag = teamTags[selectedTag];

                        if (teamTag) {

                            const logo = logoUrl + teamTag.logo;
                            const logoEl = document.createElement("img");
                            logoEl.setAttribute("src", logo);
                            tmpEle.insertBefore(logoEl, tmpEle.childNodes[0]);

                        }

                        tmpEle.appendChild(tagEle);
                        tmpEle.setAttribute("class", "example-tag");
                        state.tagsElement.appendChild(tmpEle);

                        tmpEle.onclick = () => FeedWithTabs.selectTag(selectedTag);

                        let activeTab = null;

                        for (const tab of tabs) {
                            if (tab.name === tag) {
                                activeTab = tab.name;
                            }
                        }

                        if (activeTab) {
                            tmpEle.style.display = "none";
                        }

                    }
                      
                }
            }

            state.prevTags = postData.tags;

            // Adjusts with of tags and quant container based on amount of tags/quants.
            let tagCount = (postData.tags) ? postData.tags.length : 0,
                quantCount = (postData.quants) ? postData.quants.length : 0,
                eachGet = (40 / (tagCount + quantCount)),
                baseWidth = 30;

            state.tagsElement && (state.tagsElement.style.width = baseWidth + (eachGet * tagCount) + "%");

            return element;
        }

    }

}

11. Add the comments to the top

 options.postRenderers.unshift(NcPosts.removeInstanceOf(options.postRenderers, NcPosts.CommentsRenderer));

12. Replace the default comments renderer with our own implementation of ExampleCommentsRenderer();

 NcPosts.replaceInstanceOf(options.postRenderers, NcPosts.CommentsRenderer,
        new FeedWithTabs.ExampleCommentsRenderer());

13. Our own implementation 

namespace FeedWithTabs {

    export class ExampleCommentsRenderer extends NcPosts.CommentsRenderer {

        public renderComments(comments: any, commentsContainer: HTMLDivElement) {

            const now = new Date();
            const nowDateString = now.toLocaleDateString();

            const dateFormatter = (d: Date) => {

                const dateString = d.toLocaleDateString();

                if (dateString === nowDateString) {
                    return d.toLocaleTimeString();
                } else {
                    return dateString + ", " + d.toLocaleTimeString();
                }

            };
            for (var item of comments) {
                const { name, created, comment } = item;

                var commentElem = document.createElement("div");
                commentElem.classList.add("ncpost-comment");

                // HEADER
                const header = document.createElement("div");
                header.classList.add("ncpost-comment-header");
                const icon = document.createElement("svg");
                icon.classList.add("comment-icon");
                icon.innerHTML = '<svg width="20" style="transform: scale(-1, 1)" height="20" viewBox="0 0 24 24"><path d="M12 3c5.514 0 10 3.592 10 8.007 0 4.917-5.144 7.961-9.91 7.961-1.937 0-3.384-.397-4.394-.644-1 .613-1.594 1.037-4.272 1.82.535-1.373.722-2.748.601-4.265-.837-1-2.025-2.4-2.025-4.872 0-4.415 4.486-8.007 10-8.007zm0-2c-6.338 0-12 4.226-12 10.007 0 2.05.739 4.063 2.047 5.625.055 1.83-1.023 4.456-1.993 6.368 2.602-.47 6.301-1.508 7.978-2.536 1.417.345 2.774.503 4.059.503 7.084 0 11.91-4.837 11.91-9.961-.001-5.811-5.702-10.006-12.001-10.006z"/></svg>'
                const creator = document.createElement("p");
                creator.innerHTML = name;

                const date = document.createElement("p")
                date.innerHTML = dateFormatter(new Date(created * 1000));
                date.classList.add("comment-date");

                const text = '';
                header.appendChild(icon)
                header.appendChild(creator)
                header.appendChild(date)
                commentElem.appendChild(header)

                ////

                // COMMENT
                const commentEl = document.createElement("p");
                commentEl.innerHTML = comment
                commentElem.appendChild(commentEl)
                commentsContainer.appendChild(commentElem);
                ////

            }
        }
    }
}

14. Replace the question renderer to our own implementation

 const questionRenderer = new FeedWithTabs.ExampleQuestionRenderer();
    questionRenderer.submitCommentFunc = options.submitCommentFunc;
    NcPosts.replaceInstanceOf(options.channelRenderers, NcPosts.QuestionRenderer, questionRenderer);

15. Our own question renderer:

namespace FeedWithTabs {

    export class ExampleQuestionRenderer implements NcPosts.ChannelRenderer {
        
        public submitCommentFunc: (
            name: string,
            comment: string,
            contact?: string,
            postId?: number
        ) => void = null;

        private isOpen = false;

        public shouldRun(channelContent: any): boolean {
            return channelContent.commentsEnabled;
        }

        public updateElement(
            state: any,
            channelContent: any,
            element: HTMLDivElement,
            container: HTMLDivElement,

        ) {

            if (!channelContent.commentsEnabled) {
                return
            }

            if (!element && channelContent.commentsEnabled) {
                element = document.createElement("div");
                element.className = "example-custom-comment-section";

                state.newCommentContainer = document.createElement("div");
                state.newCommentContainer.className = "comment-input-container";
                state.newCommentContainer.innerHTML = '<p class="note">Welcome in our livestudio!</p>';
                element.appendChild(state.newCommentContainer);
                state.showingNewPanel = false;
            }

            if (state.showingNewPanel !== channelContent.commentsEnabled) {
                state.showingNewPanel = channelContent.commentsEnabled;
                if (channelContent.commentsEnabled && this.submitCommentFunc) {
                    const commentBtn = document.createElement("a");

                    const openComment = document.createElement("span");
                    openComment.classList.add("arrow-comment");
                    openComment.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 27 19"><path d="M0 7.33l2.829-2.83 9.175 9.339 9.167-9.339 2.829 2.83-11.996 12.17z"/></svg>';


                    const closeComment = document.createElement("span");
                    closeComment.classList.add("arrow-comment");
                    closeComment.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 27 19"><path d="M0 16.67l2.829 2.83 9.175-9.339 9.167 9.339 2.829-2.83-11.996-12.17z"/></svg>';
                    closeComment.style.display = "none";

                    const container = document.createElement("div");
                    commentBtn.innerHTML = "Comment";

                    container.style.display = this.isOpen ? "" : "none";

                    const toggleContainer = () => {
                        this.isOpen = !this.isOpen;

                        closeComment.style.display = this.isOpen ? "" : "none";
                        openComment.style.display = this.isOpen ? "none" : "";

                        commentBtn.className = !this.isOpen ? "" : "comment-opened";
                        container.style.display = this.isOpen ? "" : "none";
                        if (state.inputValue && state.inputValue.length === 0) {
                            state.successMessage.style.display = "none";
                        }
                    }

                    commentBtn.onclick = () => {
                        return toggleContainer();

                    };

                    state.newCommentContainer.style.textAlign = !this.isOpen ? "" : "left";

                    const text1 = document.createElement("p");
                    text1.className = "example-custom-comment-name";
                    text1.innerHTML = "Name*";

                    const inputName = document.createElement("input");
                    inputName.className = "example-custom-comment-name-input";
                    inputName.value = '';
                    state.inputValue = inputName.value;

                    const text3 = document.createElement("p");
                    text3.className = "example-custom-comment";
                    text3.innerHTML = "Comment*";

                    const textArea = document.createElement("textarea");
                    textArea.className = "example-custom-comment-input";

                    const successMessage = document.createElement("p");
                    successMessage.className = "example-custom-success";
                    state.successMessage = successMessage;

                    if (inputName) {
                        successMessage.innerHTML = "Message sent";
                        successMessage.style.display = "none";
                    }

                    const buttonSend = document.createElement("button");
                    buttonSend.innerHTML = "Send";

                    buttonSend.onclick = () => {
                            try {
                                toggleContainer();
                                if (inputName.value && textArea.value) {
                                    this.submitCommentFunc(inputName.value, textArea.value);
                                    successMessage.style.display = "";
                                }

                                textArea.value = "";
                            } catch (e) {
                                console.error(e);
                            }
                    };

                    container.appendChild(text1);
                    container.appendChild(inputName);
                    container.appendChild(text3);
                    container.appendChild(textArea);
                    container.appendChild(buttonSend);
                    state.newCommentContainer.appendChild(successMessage);

                    state.newCommentContainer.appendChild(commentBtn);
                    commentBtn.appendChild(closeComment);
                    commentBtn.appendChild(openComment);
                    state.newCommentContainer.appendChild(container);
                } else {
                    state.newCommentContainer.innerHTML = "";
                    state.newCommentContainer.style.marginBottom = "0";
                }
            }

            return element;
        }
    }
}


Additional functionality

A very important question is: How to inject ads to this feed which is basically very common practice? 

We can easily customize that and inject the ad to the channel. The following example will show ad implementation with the ad on the 2nd position and from that- every 5 posts. 

We can see how the interval is set and how the feed behaves when the is post highlighted on the top. 

options.listManagerCreated = NcPosts.appendFunction(options.listManagerCreated, (listManager: NcPosts.PostListManager) => {

        type AdFactory = () => { html: string, postFunc?: () => void };

        const adFactories = {
            customAd: () => {
                const newId = "ad-example-lc-" + (++counter);
                return {
                    html: "<div id='" + newId + "'></div>",
                    postFunc: () => {

                    }
                };
            },
        }

        const buildAdElement = (adFactory: AdFactory) => {
            const adHtml = adFactory();

            return {
                element: NcHtml.create("div.ad-iframe-nt", {
                    style: "width: 100%; border: 0; text-align: center; margin-bottom: 30px",
                    _children: [
                        { _tag: "p.ncpost-content-ad", _text: "Advertisement" },
                        { _tag: "div", _html: adHtml.html }
                    ]
                }), postFunc: adHtml.postFunc
            };
        }

        const injectAdAfter = (adFactory: AdFactory, container: HTMLElement) => {
            const ad = buildAdElement(adFactory);
            container.parentNode.insertBefore(ad.element, container.nextSibling);
            ad.postFunc && ad.postFunc();
        };

        const injectAdBefore = (adFactory: AdFactory, container: HTMLElement) => {
            const ad = buildAdElement(adFactory);
            container.parentNode.insertBefore(ad.element, container);
            ad.postFunc && ad.postFunc();
        };

        let lastRollingAdBinding: NcPosts.PostListBinding = null;
        let adNettavisenFactory: AdFactory = null;

        adNettavisenFactory = adFactories.customAd;

        //We observe changes to the list and add the appropriate ads at the appropriate locations
        listManager.listChanged = NcPosts.appendFunction(listManager.listChanged, (postBindings: NcPosts.PostListBinding[]) => {

            console.log(postBindings);

            //If there's no ad set before
            if (!lastRollingAdBinding && postBindings.length >= 1) {
                lastRollingAdBinding = postBindings[0];

                if (pinnedPostExists) {
                    injectAdBefore(adNettavisenFactory, lastRollingAdBinding.container);
                } else {

                    injectAdAfter(adNettavisenFactory, lastRollingAdBinding.container);
                }

            }

            let adIndex = postBindings.indexOf(lastRollingAdBinding);
            if (adIndex  >= 0) {

                let newRollingAdBinding: NcPosts.PostListBinding;

                while (newRollingAdBinding = postBindings[adIndex  + 4]) {
                    adIndex  += 4;
                    lastRollingAdBinding = newRollingAdBinding;
                    injectAdAfter(adNettavisenFactory, lastRollingAdBinding.container);
                }
            }
        });
    });

Here you can see the result:

Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.

Still need help? How can we help? How can we help?