Adding Disqus to MiniBlog

UPDATE: I have improved on the implementation seen here in a follow up blog post: http://www.gregpakes.co.uk/post/custom-comment-engine-support-in-miniblog

A few weeks ago I migrated from WordPress to MiniBlog, more about this
here.  MiniBlog is a very minimalist blogging engine.  Minimalism is a
blessing, but also a curse.  The positive is that it has no baggage.  It
is fast, responsive and lightweight.  The negative is that it really
doesn’t support anything out of the box. 

Comments

MiniBlog comes with simple, but effective comment support.  For me it
was lacking a few features:

  • Ability to reply to a specific comment
  • Ability to receive email notifications when anyone comments or replies to a post you have commented on.
  • Spam detection / filtering

I have never used Disqus, but have seen it around and decided to give it
a try.

Setting up Disqus

Firstly, you need a Disqus account.  This is all very simple and they
even then supply installation instructions.  I chose the Discus
“Universal Code” snippets.

Modifying MiniBlog

I want to allow the user to choose which comment engine they use, so I
started by adding an application in the Web.Config file.

<add key="blog:commentEngine" value="disqus"/>  

Currently there are only two supported values:

  • Inbuilt
  • Disqus

Setting it up like this allows anyone to add further comment engines in
the future.

Comment Engines

I now needed to setup a static field in the Blog class.  This is the
class where all the configuration is kept.

public static class Blog  
{
    static Blog()
    {
        Theme = ConfigurationManager.AppSettings.Get("blog:theme");
        Title = ConfigurationManager.AppSettings.Get("blog:name");
        Description = ConfigurationManager.AppSettings.Get("blog:description");
        PostsPerPage = int.Parse(ConfigurationManager.AppSettings.Get("blog:postsPerPage"));
        DaysToComment = int.Parse(ConfigurationManager.AppSettings.Get("blog:daysToComment"));
        Image = ConfigurationManager.AppSettings.Get("blog:image");
        ModerateComments = bool.Parse(ConfigurationManager.AppSettings.Get("blog:moderateComments"));
        GoogleAnalyticsId = ConfigurationManager.AppSettings.Get("blog:googleAnalyticsId");
        GoogleAnalyticsDomain = ConfigurationManager.AppSettings.Get("blog:googleAnalyticsDomain");
        CommentEngine = ConfigurationManager.AppSettings.Get("blog:commentEngine");
    }

    public static string Title { get; private set; }
    public static string Description { get; private set; }
    public static string Theme { get; private set; }
    public static string Image { get; private set; }
    public static int PostsPerPage { get; private set; }
    public static int DaysToComment { get; private set; }
    public static bool ModerateComments { get; private set; }
    public static string GoogleAnalyticsId { get; set; }
    public static string GoogleAnalyticsDomain { get; set; }
    public static string CommentEngine { get; set; }

.....

Views

Most of the pages that needed changing were the views.  I noticed that
the comment.cshtml file in all three themes were the same.  I
decided to centralise the views for the commenting.  If they need to be
customised per theme again that would be very simple.  I created the
following folder structure.

folders

As you can see – I have removed comment.cshtml and created a
CommentEngines folder for various comment engines.  I have split the inbuilt engine into two parts:

inbuilt.cshtml – This view loops through a series of comment objects and makes calls to comment.cshtml.

@if (Model.ApprovedCommentCount > 0)
{
    <h2>Comments</h2>
}

<section id="comments" aria-label="Comments">  
    @foreach (Comment comment in Model.Comments)
    {
        if (comment.IsApproved || !Blog.ModerateComments || Context.User.Identity.IsAuthenticated)
        {
            @RenderPage("~/views/CommentEngines/inbuilt/comment.cshtml", comment)
        }
    }
</section>

@if (Model.CommentsOpen)
{
    @RenderPage("~/views/CommentForm.cshtml")
}

*

comment.cshtml – This view represents a single comment.

<article data-id="@Model.ID" itemscope itemtype="http://schema.org/UserComments" itemprop="comment" class="@(Model.IsAdmin ? "self" : null)">  
    <img src="@Model.GravatarUrl(50)" width="50" height="50" alt="Comment by @Model.Author" />
    <div>
        @Date()
        <p itemprop="commentText">@Html.Raw(Model.ContentWithLinks())</p>
        @Author()
        @DeleteAndApproveButton()
    </div>
    @ApprovalMessage()
</article>

@helper Date()
{
    var title = Model.PubDate.ToString("yyyy-MM-ddTHH:mm");
    var display = Model.PubDate.ToString("MMMM d. yyyy HH:mm");
    <time datetime="@title" itemprop="commentTime">@display</time>
}
@helper Author()
{
    if (string.IsNullOrEmpty(Model.Website))
    {
        <strong itemprop="creator">@Model.Author</strong>
    }
    else
    {
        <strong itemprop="creator"><a href="@Model.Website" itemprop="url" rel="nofollow">@Model.Author</a></strong>
    }
}
@helper DeleteAndApproveButton()
{
    if (User.Identity.IsAuthenticated)
    {
        <button class="deletecomment btn btn-link">Delete</button>
        if (Blog.ModerateComments && !Model.IsApproved)
        {
            <button class="approvecomment btn btn-link">Approve</button>
        }
    }
}
@helper ApprovalMessage()
{
    if (Blog.ModerateComments && !Model.IsApproved && !User.Identity.IsAuthenticated)
    {
        <div itemprop="approvalWarning">! The comment will not be visible until a moderator approves it !</div>
    }
}

We then had to modify post.cshtml in all three themes in order to be
able to call these new views.

@if (Blog.CurrentPost != null)
{
    switch (Blog.CommentEngine.ToLower())
    {
        case "inbuilt":
            @RenderPage("~/views/commentengines/inbuilt/inbuilt.cshtml", new
            {
                Comments = Model.Comments,
                ApprovedCommentCount = Model.CountApprovedComments(Context),
                CommentsOpen = Model.AreCommentsOpen(Context)
            })
            break;
        default:
            @RenderPage(string.Format("~/views/commentengines/{0}.cshtml", Blog.CommentEngine.ToLower()));
            break;
    }
}

As you can see, the idea is very simple here.  If we are using the
inbuilt comments engine, then we make a call to inbuilt.cshtml
passing in a custom model.  I didn’t refactor this all properly as i
didn’t really see the need.  If we are not using the inbuilt engine,
then we simply look for a view with that name and try and render it.

NB. This is not secure or particularly resistant code.  If you supply a comment engine that isn’t supported, it will fail with a lovely error message.

As I had moved the views around, I needed to correct one issue within
the commenthandler.cs which was now pointing to the wrong
comment.cshtml.

private static void RenderComment(HttpContext context, Comment comment)  
{
    var page = (WebPage)WebPageBase.CreateInstanceFromVirtualPath("~/views/commentengines/inbuilt/comment.cshtml");
    page.Context = new HttpContextWrapper(context);
    page.ExecutePageHierarchy(new WebPageContext(page.Context, page: null, model: comment), context.Response.Output);
}

Amending the comment counts

There was another issue that the comment counts were wrong.  What I did
to sort this out is quite a messy fix, but it will do for my needs.  I
added a new method to the class representing a post, post.cs.

public Uri GetCommentUri()  
{
    var hashComment = "comments";

    if (Blog.CommentEngine.ToLower() == "disqus")
    {
        hashComment = "disqus_thread ";
    }

    var uri = new Uri(VirtualPathUtility.ToAbsolute("~/post/" + Slug + "#" + hashComment), UriKind.Relative);
    return uri;

}

Once again, I am not happy with the code above.  It violates the “hollywood principle”, but again, it will do for our needs.

I then edited post.cshtml in all three themes again.

<a href="@Model.GetCommentUri()">  
    <em class="glyphicon glyphicon-comment"></em>
    @Model.CountApprovedComments(Context) Comments
</a>  

Now the disqus comments work.

Restricting the Comments handler

I wanted to make sure that the inbuilt comments handler was not
available unless the inbuilt comments engine was in use.  i located the
file commenthandler.cs and simple added a few lines at the top.

public class CommentHandler : IHttpHandler  
{
    public void ProcessRequest(HttpContext context)
    {
        // Disable the CommentHandler if the inbuilt comment engine is not being used.
        if (Blog.CommentEngine.ToLower() != "inbuilt")
        {
            throw new HttpException(400, "The inbuilt comment engine is disabled.");
        }

        Post post = Storage.GetAllPosts().FirstOrDefault(p => p.ID == context.Request["postId"]);

...

Disqus Support

Now I needed to add disqus support.  I started by adding another
application setting into the web.config to hold the disqus shortname.

<add key="disqus:shortname" value="ENTER YOUR SHORTNAME HERE"/>  

I went back to the blog class and added support for this setting.  I
realise this is not very clean and it would be nicer to have the disqus
settings kept away from the blog settings, but I just don’t care.

public static class Blog  
{
    static Blog()
    {
        Theme = ConfigurationManager.AppSettings.Get("blog:theme");
        Title = ConfigurationManager.AppSettings.Get("blog:name");
        Description = ConfigurationManager.AppSettings.Get("blog:description");
        PostsPerPage = int.Parse(ConfigurationManager.AppSettings.Get("blog:postsPerPage"));
        DaysToComment = int.Parse(ConfigurationManager.AppSettings.Get("blog:daysToComment"));
        Image = ConfigurationManager.AppSettings.Get("blog:image");
        ModerateComments = bool.Parse(ConfigurationManager.AppSettings.Get("blog:moderateComments"));
        GoogleAnalyticsId = ConfigurationManager.AppSettings.Get("blog:googleAnalyticsId");
        GoogleAnalyticsDomain = ConfigurationManager.AppSettings.Get("blog:googleAnalyticsDomain");
        CommentEngine = ConfigurationManager.AppSettings.Get("blog:commentEngine");
        DisqusShortName = ConfigurationManager.AppSettings.Get("disqus:shortname");
    }

    public static string Title { get; private set; }
    public static string Description { get; private set; }
    public static string Theme { get; private set; }
    public static string Image { get; private set; }
    public static int PostsPerPage { get; private set; }
    public static int DaysToComment { get; private set; }
    public static bool ModerateComments { get; private set; }
    public static string GoogleAnalyticsId { get; set; }
    public static string GoogleAnalyticsDomain { get; set; }
    public static string CommentEngine { get; set; }
    public static string DisqusShortName { get; set; }

The View

Now I needed to create the Disqus view, disqus.cshtml.

<h2>Comments</h2>  
<section id="comments" aria-label="Comments">  
    <div id="disqus_thread"></div>
</section>  
<script type="text/javascript">  
    /* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */
    var disqus_shortname = '@Blog.DisqusShortName'; // required: replace example with your forum shortname

    /* * * DON'T EDIT BELOW THIS LINE * * */
    (function () {
        var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
        dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
        (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
    })();
</script>  
<noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>  
<a href="http://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>

<script type="text/javascript">  
    /* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */
    var disqus_shortname = '@Blog.DisqusShortName'; // required: replace example with your forum shortname

    /* * * DON'T EDIT BELOW THIS LINE * * */
    (function () {
        var s = document.createElement('script'); s.async = true;
        s.type = 'text/javascript';
        s.src = '//' + disqus_shortname + '.disqus.com/count.js';
        (document.getElementsByTagName('HEAD')[0] || document.getElementsByTagName('BODY')[0]).appendChild(s);
    }());
</script>  

Voila

Now all I need to do is set the comment engine configuration setting to
“disqus” and I would get disqus comments on my blog.

I still need to import the comments from my old blog.  I will write
about that later.

I will commit all these changes to the GitHub repository when they are
all finished.  https://github.com/gregpakes/MiniBlog