diff --git a/client/css/colors.styl b/client/css/colors.styl
index b4b2756..1a5996e 100644
--- a/client/css/colors.styl
+++ b/client/css/colors.styl
@@ -1,4 +1,5 @@
 $main-color = #24AADD
+$window-color = white
 $top-nav-color = #F5F5F5
 $text-color = #111
 $inactive-link-color = #888
diff --git a/client/css/comments.styl b/client/css/comments.styl
new file mode 100644
index 0000000..a3d5280
--- /dev/null
+++ b/client/css/comments.styl
@@ -0,0 +1,136 @@
+@import colors
+
+.comments>ul
+    list-style-type: none
+    margin: 1em 0
+    padding: 0
+
+.comment
+    margin: 0 0 1em 0
+    padding: 0
+    display: -webkit-flex
+    display: flex
+
+    &:not(.editing)
+        .tabs nav
+            display: none
+        .tabs .edit.tab
+            display: none
+    &.editing
+        .tab:not(.active)
+            display: none
+        .tabs-wrapper
+            background: $active-tab-background-color
+        .tab
+            padding: 1em
+            .content-wrapper
+                background: $window-color
+                overflow: hidden
+                .content
+                    margin: 1em
+            textarea
+                resize: vertical
+                width: 100%
+                max-height: 80vh
+                box-sizing: padding-box
+
+    .avatar
+        margin-right: 1em
+        -webkit-flex-shrink: 0
+        flex-shrink: 0
+        vertical-align: top
+
+        .thumbnail
+            width: 40px
+            height: 40px
+            margin: 0
+
+    .body
+        width: 100%
+
+        header
+            line-height: 16pt
+            vertical-align: middle
+            margin-bottom: 0.5em
+            background: $top-nav-color
+            padding: 0.2em 0.5em
+
+            .date, .score-container, .edit, .delete
+                margin-left: 2em
+                font-size: 95%
+            .edit, .delete, .score-container a, .nickname a
+                color: mix($main-color, $inactive-tab-text-color)
+            .edit, .delete
+                font-size: 80%
+
+            i
+                margin-right: 0.3em
+            .downvote i
+                text-align: right
+            .upvote i
+                display: inline-block
+                width: 1em
+                margin: 0
+            .value
+                text-align: center
+                display: inline-block
+                width: 2em
+
+        form
+            width: auto
+            margin: 0
+
+        nav
+            vertical-align: middle
+            margin: 0 0.8em 0.5em 0
+            &.buttons
+                float: left
+            &.actions
+                float: left
+                margin-top: 0.3em
+
+        .messages
+            margin: 1em 0
+
+    .content
+        ul
+            list-style-position: inside
+            margin: 1em 0
+            padding: 0
+
+        .sjis
+            font-family: 'MS PGothic', 'MS Pゴシック', 'IPAMonaPGothic', 'Trebuchet MS', Verdana, Futura, Arial, Helvetica, sans-serif
+            background: #fbfbfb
+            color: #111
+            font-size: 12pt
+            line-height: 1
+            margin: 0
+            padding: 4px
+            overflow: auto
+            white-space: pre
+            word-wrap: normal
+
+        p:first-child
+            margin-top: 0
+
+        .spoiler
+            background: #eee
+            color: #eee
+            &:hover
+                color: dimgray
+            &:before
+                content: '['
+                color: #000
+            &:after
+                content: ']'
+                color: #000
+
+        blockquote
+            border-left: 3px solid #eee
+            margin-left: 0
+            padding: 0.3em 0.3em 0.3em 0.7em
+            background: #fafafa
+            color: #444
+
+        blockquote :last-child
+            margin-bottom: 0
diff --git a/client/css/forms.styl b/client/css/forms.styl
index 62c2119..f62b1d1 100644
--- a/client/css/forms.styl
+++ b/client/css/forms.styl
@@ -270,6 +270,11 @@ input[type=submit]
         background-color: $button-disabled-background-color
         color: $button-disabled-text-color
 
+    &.discourage
+        border-color: transparent
+        background-color: transparent
+        color: $button-disabled-text-color
+
     &:focus
         border: 2px solid $text-color
 
diff --git a/client/css/main.styl b/client/css/main.styl
index af7dff9..699be1e 100644
--- a/client/css/main.styl
+++ b/client/css/main.styl
@@ -15,6 +15,7 @@ body
     min-height: 100%
 
 body
+    background: $window-color
     overflow-y: scroll
     margin: 0
     color: $text-color
diff --git a/client/css/posts.styl b/client/css/posts.styl
index fbde811..877f001 100644
--- a/client/css/posts.styl
+++ b/client/css/posts.styl
@@ -65,7 +65,7 @@ $safety-unsafe = #F3985F
 
             .social
                 margin-top: 1em
-                .score
+                .score-container
                     float: left
                     margin-right: 3em
                     .downvote i
diff --git a/client/html/comment.tpl b/client/html/comment.tpl
new file mode 100644
index 0000000..aac6346
--- /dev/null
+++ b/client/html/comment.tpl
@@ -0,0 +1,75 @@
+<div class='comment'>
+    <div class='avatar'>
+        <% if (ctx.comment.user.name && ctx.canViewUsers) { %>
+            <a href='/user/<%= ctx.comment.user.name %>'>
+        <% } %>
+
+        <%= ctx.makeThumbnail(ctx.comment.user.avatarUrl) %>
+
+        <% if (ctx.comment.user.name && ctx.canViewUsers) { %>
+            </a>
+        <% } %>
+    </div>
+
+    <div class='body'>
+        <header><!--
+            --><span class='nickname'><!--
+                --><% if (ctx.comment.user.name && ctx.canViewUsers) { %><!--
+                    --><a href='/user/<%= ctx.comment.user.name %>'><!--
+                --><% } %><!--
+
+                --><%= ctx.comment.user.name %><!--
+
+                --><% if (ctx.comment.user.name && ctx.canViewUsers) { %><!--
+                    --></a><!--
+                --><% } %><!--
+            --></span><!--
+
+            --><span class='date'><!--
+                --><%= ctx.makeRelativeTime(ctx.comment.creationTime) %><!--
+            --></span><!--
+
+            --><span class='score-container'></span><!--
+
+            --><% if (ctx.canEditComment) { %><!--
+                --><a class='edit' href='#'><!--
+                    --><i class='fa fa-pencil'></i> edit<!--
+                --></a><!--
+            --><% } %><!--
+
+            --><% if (ctx.canDeleteComment) { %><!--
+                --><a class='delete' href='#'><!--
+                    --><i class='fa fa-remove'></i> delete<!--
+                --></a><!--
+            --><% } %><!--
+        --></header>
+
+        <div class='tabs'>
+            <form>
+                <div class='tabs-wrapper'>
+                    <div class='preview tab'>
+                        <div class='content-wrapper'><div class='content'><%= ctx.makeMarkdown(ctx.comment.text) %></div></div>
+                    </div>
+
+                    <div class='edit tab'>
+                        <textarea required minlength=1><%= ctx.comment.text %></textarea>
+                    </div>
+                </div>
+
+                <nav class='buttons'>
+                    <ul>
+                        <li class='preview'><a href='#'>Preview</a></li>
+                        <li class='edit'><a href='#'>Edit</a></li>
+                    </ul>
+                </nav>
+
+                <nav class='actions'>
+                    <input type='submit' class='save' value='Save'/>
+                    <input type='button' class='cancel discourage' value='Cancel'/>
+                </nav>
+            </form>
+
+            <div class='messages'></div>
+        </div>
+    </div>
+</div>
diff --git a/client/html/comment_list.tpl b/client/html/comment_list.tpl
new file mode 100644
index 0000000..5e33458
--- /dev/null
+++ b/client/html/comment_list.tpl
@@ -0,0 +1,6 @@
+<div class='comments'>
+    <% if (ctx.canListComments && ctx.comments.length) { %>
+        <ul>
+        </ul>
+    <% } %>
+</div>
diff --git a/client/html/fav.tpl b/client/html/fav.tpl
new file mode 100644
index 0000000..28426e2
--- /dev/null
+++ b/client/html/fav.tpl
@@ -0,0 +1,15 @@
+<% if (ctx.canFavorite) { %>
+    <% if (ctx.ownFavorite) { %>
+        <a class='remove-favorite' href='#'>
+            <i class='fa fa-heart'></i>
+    <% } else { %>
+        <a class='add-favorite' href='#'>
+            <i class='fa fa-heart-o'></i>
+    <% } %>
+<% } else { %>
+    <a class='add-favorite inactive'>
+        <i class='fa fa-heart-o'></i>
+<% } %>
+    <span class='vim-nav-hint'>add to favorites</span>
+</a>
+<span class='value'><%= ctx.favoriteCount %></span>
diff --git a/client/html/post.tpl b/client/html/post.tpl
index 0075777..39bf9d6 100644
--- a/client/html/post.tpl
+++ b/client/html/post.tpl
@@ -46,8 +46,6 @@
     <div class='content'>
         <div class='post-container'></div>
 
-        <section class='comments'>
-            <!-- TODO: comments -->
-        </section>
+        <div class='comments-container'></div>
     </div>
 </div>
diff --git a/client/html/post_readonly_sidebar.tpl b/client/html/post_readonly_sidebar.tpl
index bd03475..0922e26 100644
--- a/client/html/post_readonly_sidebar.tpl
+++ b/client/html/post_readonly_sidebar.tpl
@@ -38,53 +38,9 @@
     </section>
 
     <section class='social'>
-        <div class='score'>
-            <% if (ctx.canScorePosts) { %>
-                <a class='upvote' href='#'>
-                    <% if (ctx.post.ownScore == 1) { %>
-                        <i class='fa fa-thumbs-up'></i>
-                    <% } else { %>
-                        <i class='fa fa-thumbs-o-up'></i>
-                    <% } %>
-                    <span class='vim-nav-hint'>upvote</span>
-                    <span class='vim-nav-hint'>like</span>
-                </a>
-            <% } else { %>
-                <a class='upvote inactive'>
-                    <i class='fa fa-thumbs-o-up'></i>
-                </a>
-            <% } %>
-            <span class='value'><%= ctx.post.score %></span>
-            <% if (ctx.canScorePosts) { %>
-                <a class='downvote' href='#'>
-                    <% if (ctx.post.ownScore == -1) { %>
-                        <i class='fa fa-thumbs-down'></i>
-                    <% } else { %>
-                        <i class='fa fa-thumbs-o-down'></i>
-                    <% } %>
-                    <span class='vim-nav-hint'>downvote</span>
-                    <span class='vim-nav-hint'>dislike</span>
-                </a>
-            <% } %>
-        </div>
+        <div class='score-container'></div>
 
-        <div class='fav'>
-            <% if (ctx.canFavoritePosts) { %>
-                <% if (ctx.post.ownFavorite) { %>
-                    <a class='remove-favorite' href='#'>
-                        <i class='fa fa-heart'></i>
-                <% } else { %>
-                    <a class='add-favorite' href='#'>
-                        <i class='fa fa-heart-o'></i>
-                <% } %>
-            <% } else { %>
-                <a class='add-favorite inactive'>
-                    <i class='fa fa-heart-o'></i>
-            <% } %>
-                <span class='vim-nav-hint'>add to favorites</span>
-            </a>
-            <span class='value'><%= ctx.post.favoriteCount %></span>
-        </div>
+        <div class='fav-container'></div>
     </section>
 </article>
 
diff --git a/client/html/score.tpl b/client/html/score.tpl
new file mode 100644
index 0000000..23e2219
--- /dev/null
+++ b/client/html/score.tpl
@@ -0,0 +1,27 @@
+<% if (ctx.canScore) { %>
+    <a class='upvote' href='#'>
+        <% if (ctx.ownScore == 1) { %>
+            <i class='fa fa-thumbs-up'></i>
+        <% } else { %>
+            <i class='fa fa-thumbs-o-up'></i>
+        <% } %>
+        <span class='vim-nav-hint'>upvote</span>
+        <span class='vim-nav-hint'>like</span>
+    </a>
+<% } else { %>
+    <a class='upvote inactive'>
+        <i class='fa fa-thumbs-o-up'></i>
+    </a>
+<% } %>
+<span class='value'><%= ctx.score %></span>
+<% if (ctx.canScore) { %>
+    <a class='downvote' href='#'>
+        <% if (ctx.ownScore == -1) { %>
+            <i class='fa fa-thumbs-down'></i>
+        <% } else { %>
+            <i class='fa fa-thumbs-o-down'></i>
+        <% } %>
+        <span class='vim-nav-hint'>downvote</span>
+        <span class='vim-nav-hint'>dislike</span>
+    </a>
+<% } %>
diff --git a/client/js/controls/comment_control.js b/client/js/controls/comment_control.js
new file mode 100644
index 0000000..5ca252a
--- /dev/null
+++ b/client/js/controls/comment_control.js
@@ -0,0 +1,185 @@
+'use strict';
+
+const api = require('../api.js');
+const misc = require('../util/misc.js');
+const views = require('../util/views.js');
+
+class CommentControl {
+    constructor(hostNode, comment) {
+        this._hostNode = hostNode;
+        this._comment = comment;
+        this._template = views.getTemplate('comment');
+        this._scoreTemplate = views.getTemplate('score');
+
+        this.install();
+    }
+
+    install() {
+        const isLoggedIn = api.isLoggedIn(this._comment.user);
+        const infix = isLoggedIn ? 'own' : 'any';
+        const sourceNode = this._template({
+            comment: this._comment,
+            canViewUsers: api.hasPrivilege('users:view'),
+            canEditComment: api.hasPrivilege(`comments:edit:${infix}`),
+            canDeleteComment: api.hasPrivilege(`comments:delete:${infix}`),
+        });
+
+        views.showView(
+            sourceNode.querySelector('.score-container'),
+            this._scoreTemplate({
+                score: this._comment.score,
+                ownScore: this._comment.ownScore,
+                canScore: api.hasPrivilege('comments:score'),
+            }));
+
+        const editButton = sourceNode.querySelector('.edit');
+        const deleteButton = sourceNode.querySelector('.delete');
+        const upvoteButton = sourceNode.querySelector('.upvote');
+        const downvoteButton = sourceNode.querySelector('.downvote');
+        const previewTabButton = sourceNode.querySelector('.buttons .preview');
+        const editTabButton = sourceNode.querySelector('.buttons .edit');
+        const formNode = sourceNode.querySelector('form');
+        const cancelButton = sourceNode.querySelector('.cancel');
+        const textareaNode = sourceNode.querySelector('form textarea');
+
+        if (editButton) {
+            editButton.addEventListener(
+                'click', e => this._evtEditClick(e));
+        }
+        if (deleteButton) {
+            deleteButton.addEventListener(
+                'click', e => this._evtDeleteClick(e));
+        }
+
+        if (upvoteButton) {
+            upvoteButton.addEventListener(
+                'click',
+                e => this._evtScoreClick(
+                    e, () => this._comment.ownScore === 1 ? 0 : 1));
+        }
+        if (downvoteButton) {
+            downvoteButton.addEventListener(
+                'click',
+                e => this._evtScoreClick(
+                    e, () => this._comment.ownScore === -1 ? 0 : -1));
+        }
+
+        previewTabButton.addEventListener(
+            'click', e => this._evtPreviewClick(e));
+        editTabButton.addEventListener(
+            'click', e => this._evtEditClick(e));
+
+        formNode.addEventListener('submit', e => this._evtSaveClick(e));
+        cancelButton.addEventListener('click', e => this._evtCancelClick(e));
+
+        for (let event of ['cut', 'paste', 'drop', 'keydown']) {
+            textareaNode.addEventListener(event, e => {
+                window.setTimeout(() => this._growTextArea(), 0);
+            });
+        }
+        textareaNode.addEventListener('change', e => { this._growTextArea(); });
+
+        views.showView(this._hostNode, sourceNode);
+    }
+
+    _evtScoreClick(e, scoreGetter) {
+        e.preventDefault();
+        api.put(
+            '/comment/' + this._comment.id + '/score',
+            {score: scoreGetter()})
+        .then(
+            response => {
+                this._comment.score = parseInt(response.score);
+                this._comment.ownScore = parseInt(response.ownScore);
+                this.install();
+            }, response => {
+                window.alert(response.description);
+            });
+    }
+
+    _evtDeleteClick(e) {
+        e.preventDefault();
+        if (!window.confirm('Are you sure you want to delete this comment?')) {
+            return;
+        }
+        api.delete('/comment/' + this._comment.id)
+            .then(response => {
+                this._hostNode.parentNode.removeChild(this._hostNode);
+            }, response => {
+                window.alert(response.description);
+            });
+    }
+
+    _evtSaveClick(e) {
+        e.preventDefault();
+        api.put('/comment/' + this._comment.id, {
+            text: this._hostNode.querySelector('.edit.tab textarea').value,
+        }).then(response => {
+            this._comment = response;
+            this.install();
+        }, response => {
+            this._showError(response.description);
+        });
+    }
+
+    _evtPreviewClick(e) {
+        e.preventDefault();
+        this._hostNode.querySelector('.preview.tab .content').innerHTML
+            = misc.formatMarkdown(
+                this._hostNode.querySelector('.edit.tab textarea').value);
+        this._freezeTabHeights();
+        this._selectTab('preview');
+    }
+
+    _evtEditClick(e) {
+        e.preventDefault();
+        this._freezeTabHeights();
+        this._enterEditMode();
+        this._selectTab('edit');
+        this._growTextArea();
+    }
+
+    _evtCancelClick(e) {
+        e.preventDefault();
+        this._exitEditMode();
+        this._hostNode.querySelector('.edit.tab textarea').value
+            = this._comment.text;
+    }
+
+    _enterEditMode() {
+        this._hostNode.querySelector('.comment').classList.add('editing');
+        misc.enableExitConfirmation();
+    }
+
+    _exitEditMode() {
+        this._hostNode.querySelector('.comment').classList.remove('editing');
+        this._hostNode.querySelector('.tabs-wrapper').style.minHeight = null;
+        misc.disableExitConfirmation();
+        views.clearMessages(this._hostNode);
+    }
+
+    _selectTab(tabName) {
+        this._freezeTabHeights();
+        for (let tab of this._hostNode.querySelectorAll('.tab, .buttons li')) {
+            tab.classList.toggle('active', tab.classList.contains(tabName));
+        }
+    }
+
+    _freezeTabHeights() {
+        const tabsNode = this._hostNode.querySelector('.tabs-wrapper');
+        const tabsHeight = tabsNode.getBoundingClientRect().height;
+        tabsNode.style.minHeight = tabsHeight + 'px';
+    }
+
+    _growTextArea() {
+        const previewNode = this._hostNode.querySelector('.content');
+        const textareaNode = this._hostNode.querySelector('textarea');
+        textareaNode.style.height = textareaNode.scrollHeight + 'px';
+    }
+
+    _showError(message) {
+        views.showError(this._hostNode, message);
+    }
+};
+
+module.exports = CommentControl;
diff --git a/client/js/controls/comment_list_control.js b/client/js/controls/comment_list_control.js
new file mode 100644
index 0000000..d78a6ad
--- /dev/null
+++ b/client/js/controls/comment_list_control.js
@@ -0,0 +1,41 @@
+'use strict';
+
+const api = require('../api.js');
+const views = require('../util/views.js');
+const CommentControl = require('../controls/comment_control.js');
+
+class CommentListControl {
+    constructor(hostNode, comments) {
+        this._hostNode = hostNode;
+        this._comments = comments;
+        this._template = views.getTemplate('comment-list');
+
+        this.install();
+    }
+
+    install() {
+        const sourceNode = this._template({
+            comments: this._comments,
+            canListComments: api.hasPrivilege('comments:list'),
+        });
+
+        views.showView(this._hostNode, sourceNode);
+
+        this._renderComments();
+    }
+
+    _renderComments() {
+        if (!this._comments.length) {
+            return;
+        }
+        const commentList = new DocumentFragment();
+        for (let comment of this._comments) {
+            const commentListItemNode = document.createElement('li');
+            new CommentControl(commentListItemNode, comment);
+            commentList.appendChild(commentListItemNode);
+        }
+        views.showView(this._hostNode.querySelector('ul'), commentList);
+    }
+};
+
+module.exports = CommentListControl;
diff --git a/client/js/controls/post_readonly_sidebar_control.js b/client/js/controls/post_readonly_sidebar_control.js
index f8d523b..4ae8f64 100644
--- a/client/js/controls/post_readonly_sidebar_control.js
+++ b/client/js/controls/post_readonly_sidebar_control.js
@@ -10,6 +10,8 @@ class PostReadonlySidebarControl {
         this._post = post;
         this._postContentControl = postContentControl;
         this._template = views.getTemplate('post-readonly-sidebar');
+        this._scoreTemplate = views.getTemplate('score');
+        this._favTemplate = views.getTemplate('fav');
 
         this.install();
     }
@@ -20,10 +22,25 @@ class PostReadonlySidebarControl {
             getTagCategory: this._getTagCategory,
             getTagUsages: this._getTagUsages,
             canListPosts: api.hasPrivilege('posts:list'),
-            canScorePosts: api.hasPrivilege('posts:score'),
-            canFavoritePosts: api.hasPrivilege('posts:favorite'),
             canViewTags: api.hasPrivilege('tags:view'),
         });
+
+        views.showView(
+            sourceNode.querySelector('.score-container'),
+            this._scoreTemplate({
+                score: this._post.score,
+                ownScore: this._post.ownScore,
+                canScore: api.hasPrivilege('posts:score'),
+            }));
+
+        views.showView(
+            sourceNode.querySelector('.fav-container'),
+            this._favTemplate({
+                favoriteCount: this._post.favoriteCount,
+                ownFavorite: this._post.ownFavorite,
+                canFavorite: api.hasPrivilege('posts:favorite'),
+            }));
+
         const upvoteButton = sourceNode.querySelector('.upvote');
         const downvoteButton = sourceNode.querySelector('.downvote')
         const addFavButton = sourceNode.querySelector('.add-favorite')
diff --git a/client/js/main.js b/client/js/main.js
index 1636e96..7afb1e0 100644
--- a/client/js/main.js
+++ b/client/js/main.js
@@ -1,6 +1,7 @@
 'use strict';
 
 require('./util/polyfill.js');
+const misc = require('./util/misc.js');
 
 const page = require('page');
 const origPushState = page.Context.prototype.pushState;
@@ -9,6 +10,20 @@ page.Context.prototype.pushState = function() {
     origPushState.call(this);
 };
 
+page.cancel = function(ctx) {
+    prevContext = ctx;
+    ctx.pushState();
+};
+
+page.exit((ctx, next) => {
+    views.unlistenToMessages();
+    if (misc.confirmPageExit()) {
+        next();
+    } else {
+        page.cancel(ctx);
+    }
+});
+
 const mousetrap = require('mousetrap');
 page(/.*/, (ctx, next) => {
     mousetrap.reset();
@@ -34,11 +49,6 @@ for (let controller of controllers) {
     controller.registerRoutes();
 }
 
-page.exit((ctx, next) => {
-    views.unlistenToMessages();
-    next();
-});
-
 const api = require('./api.js');
 Promise.all([tags.refreshExport(), api.loginFromCookies()])
     .then(() => {
diff --git a/client/js/util/misc.js b/client/js/util/misc.js
index 68af2d8..54c0731 100644
--- a/client/js/util/misc.js
+++ b/client/js/util/misc.js
@@ -199,6 +199,27 @@ function unindent(callSite, ...args) {
     return format(output);
 }
 
+function enableExitConfirmation() {
+    window.onbeforeunload = e => {
+        return 'Are you sure you want to leave? ' +
+            'Data you have entered may not be saved.';
+    };
+}
+
+function disableExitConfirmation() {
+    window.onbeforeunload = null;
+}
+
+function confirmPageExit() {
+    if (!window.onbeforeunload) {
+        return true;
+    }
+    if (window.confirm(window.onbeforeunload())) {
+        disableExitConfirmation();
+        return true;
+    }
+}
+
 module.exports = {
     range: range,
     formatSearchQuery: formatSearchQuery,
@@ -208,4 +229,7 @@ module.exports = {
     formatFileSize: formatFileSize,
     formatMarkdown: formatMarkdown,
     unindent: unindent,
+    enableExitConfirmation: enableExitConfirmation,
+    disableExitConfirmation: disableExitConfirmation,
+    confirmPageExit: confirmPageExit,
 };
diff --git a/client/js/util/views.js b/client/js/util/views.js
index 940d035..9ad5775 100644
--- a/client/js/util/views.js
+++ b/client/js/util/views.js
@@ -29,6 +29,10 @@ function makeFileSize(fileSize) {
     return misc.formatFileSize(fileSize);
 }
 
+function makeMarkdown(text) {
+    return misc.formatMarkdown(text);
+}
+
 function makeRelativeTime(time) {
     return makeNonVoidElement(
         'time',
@@ -202,7 +206,7 @@ function makeVoidElement(name, attributes) {
     return `<${_serializeElement(name, attributes)}/>`;
 }
 
-function _messageHandler(target, message, className) {
+function showMessage(target, message, className) {
     if (!message) {
         message = 'Unknown message';
     }
@@ -222,6 +226,18 @@ function _messageHandler(target, message, className) {
     return true;
 }
 
+function showError(target, message) {
+    return showMessage(target, message, 'error');
+}
+
+function showSuccess(target, message) {
+    return showMessage(target, message, 'success');
+}
+
+function showInfo(target, message) {
+    return showMessage(target, message, 'info');
+}
+
 function unlistenToMessages() {
     events.unlisten(events.Success);
     events.unlisten(events.Error);
@@ -234,7 +250,7 @@ function listenToMessages(target) {
         events.listen(
             eventType,
             msg => {
-                return _messageHandler(target, msg, className);
+                return showMessage(target, msg, className);
             });
     };
     listen(events.Success, 'success');
@@ -269,6 +285,7 @@ function getTemplate(templatePath) {
         Object.assign(ctx, {
             makeRelativeTime: makeRelativeTime,
             makeFileSize: makeFileSize,
+            makeMarkdown: makeMarkdown,
             makeThumbnail: makeThumbnail,
             makeRadio: makeRadio,
             makeCheckbox: makeCheckbox,
@@ -420,4 +437,7 @@ module.exports = {
     slideDown: slideDown,
     slideUp: slideUp,
     monitorNodeRemoval: monitorNodeRemoval,
+    showError: showError,
+    showSuccess: showSuccess,
+    showInfo: showInfo,
 };
diff --git a/client/js/views/post_view.js b/client/js/views/post_view.js
index f8e25df..0c7b541 100644
--- a/client/js/views/post_view.js
+++ b/client/js/views/post_view.js
@@ -10,6 +10,7 @@ const PostReadonlySidebarControl
     = require('../controls/post_readonly_sidebar_control.js');
 const PostEditSidebarControl
     = require('../controls/post_edit_sidebar_control.js');
+const CommentListControl = require('../controls/comment_list_control.js');
 
 class PostView {
     constructor() {
@@ -63,6 +64,10 @@ class PostView {
                 this._postContentControl);
         }
 
+        new CommentListControl(
+            postViewNode.querySelector('.comments-container'),
+            ctx.post.comments);
+
         keyboard.bind('e', () => {
             if (ctx.editMode) {
                 page.show('/post/' + ctx.post.id);