Bloggerテンプレート自作 #14:コメントフォームを設置する

前回の「関連記事を設置する」に引き続き、今回は「コメントフォームを設置する」を実践してみようと思う。

コメントフォームはBloggerの機能として実装されているものの、標準のコメントガジェットや特定の呼び出し関数なんかは用意されていないっぽい、なのでBloggerテンプレートを自作する際には自力で設置するコードを記述しなければならないようだ。今回は、その「コメントを設置するコード」についてまとめておくことにする。


Bloggerのコメントフォームについて


どこにも説明がない


コメントフォームを設置する方法は、きっと公式ドキュメントで解説されているだろうと思いきや、どこを見ても全く触れられていない。個人サイトでも具体的に触れている記事は見つからなかったので、結局 既存のテンプレートを解析してそれっぽいコードを記述し、なんとか動くところまで漕ぎつけた。なので、実装できているものの、詳しいことは分かっていない。

コメント入力フォームを呼び出す方法


<b:if cond='data:post.allowComments'>
  <b:include data='post' name='comment_picker'/>
</b:if>

コードを解析したところ、どうやらこれがコメント入力フォーム(コメントピッカー)を表示させるコードらしい。

これを記述してみたところ、とりあえずコメント入力フォームは現れてくれた。しかし、この状態でいくらコメントを打ってみても全く保存されない。なので、コメントを保存するには別の記述が必要なようだ。

なお、冒頭の条件分岐は「コメントの許可」の可否を判断するコードだと思われる。上記のコードを記述した上でコメント無効のページを見ても、コメント入力フォームは表示されない。

コメントを保存できるようにする記述


<!-- コメントを保存できるようにするコード1 -->
<b:includable id='comment-form' var='post'>
  <div class='comment-form'>
    <a name='comment-form' />
    <b:if cond='data:mobile'>
      <h4 id='comment-post-message'>
<!-- 以下略 -->

<!-- コメントを保存できるようにするコード2 -->
<b:includable id='threaded-comment-form' var='post'>
  <div class='comment-form'>
    <a name='comment-form' />
    <b:if cond='data:mobile'>
<!-- 以下略 -->

どうやら、上記がコメントを保存できるようにするコードらしい(全文は下記を参照)。

最初は、この <b:includable id='comment-form' var='post'> から始まる一連のコードをコピペしてコメントが保存できるかどうかを試してみた。その結果は「失敗」、何も変化はなかった。

そこで、先のコードを残しつつ <b:includable id='threaded-comment-form' var='post'> から始まる一連のコードをコピペしてコメントが保存できるかどうかを試してみた。その結果は「成功」、なんと成功してしまった。とりあえず、返信なんかも試してみたら、これもできるらしい。

らしいばっかりで申し訳ないが、理解できていないのでこればかりは仕方がない。Bloggerは公式ドキュメントはこの辺もまとめて説明しておくべきだと思うよ。マジで。

作業工程


コメント表示欄を作る


<!-- 個別記事ページ -->
<b:includable id='single-post'>

  <!-- 個別記事ページの記述(省略) -->

  <!-- コメントを読み込む -->
  <b:include name='post-comments' />
</b:includable>

<!-- コメントピッカー -->
<b:includable id='post-comments'>
  <b:if cond='data:post.allowComments'>
    <div class="comment-wrap">
      <h3>コメント</h3>
      <div class='comment-area'>
        <b:include data='post' name='comment_picker'/>
      </div>
    </div>
   </b:if>
</b:includable>

上記がコメント表示欄を設けるコードになる。

<b:includable id='post-comments'> ~ </b:includable>がコメント表示欄をひとまとまりにしたコード。これを <b:include name='post-comments' /> で呼び出せるので、個別記事ページ内の表示したい部分に記述すればOK。

コメント保存用のコードを記述する


<!-- コメントの構成コードはじめ -->
<b:includable id='comment-form' var='post'>
  <div class='comment-form'>
    <a name='comment-form'/>
    <b:if cond='data:mobile'>
      <h4 id='comment-post-message'>
        <a expr:id='data:widget.instanceId + &quot;_comment-editor-toggle-link&quot;' href='javascript:void(0)'>
          <data:postCommentMsg/>
        </a>
      </h4>
      <p>
        <data:blogCommentMessage/>
      </p>
      <data:blogTeamBlogMessage/>
      <a expr:href='data:post.commentFormIframeSrc' id='comment-editor-src'/>
      <iframe allowtransparency='true' class='blogger-iframe-colorize blogger-comment-from-post' frameborder='0' height='410' id='comment-editor' name='comment-editor' src='' style='display: none' width='100%'/>
      <b:else/>
      <h4 id='comment-post-message'>
        <data:postCommentMsg/>
      </h4>
      <p>
        <data:blogCommentMessage/>
      </p>
      <data:blogTeamBlogMessage/>
      <a expr:href='data:post.commentFormIframeSrc' id='comment-editor-src'/>
      <iframe allowtransparency='true' class='blogger-iframe-colorize blogger-comment-from-post' frameborder='0' height='410' id='comment-editor' name='comment-editor' src='' width='100%'/>
    </b:if>
    <data:post.friendConnectJs/>
    <data:post.cmtfpIframe/>
    <script type='text/javascript'>
      BLOG_CMT_createIframe(&#39;<data:post.appRpcRelayPath/>&#39;);
    </script>
  </div>
</b:includable>
<b:includable id='commentDeleteIcon' var='comment'>
</b:includable>
<b:includable id='comment_count_picker' var='post'>
  <b:if cond='data:post.commentSource == 1'>
    <span class='cmt_count_iframe_holder' expr:data-count='data:post.numComments' expr:data-onclick='data:post.addCommentOnclick' expr:data-post-url='data:post.url' expr:data-url='data:post.canonicalUrl'>
    </span>
    <b:else/>
    <a class='comment-link' expr:href='data:post.addCommentUrl' expr:onclick='data:post.addCommentOnclick'>
      <data:post.commentLabelFull/>
      :
    </a>
  </b:if>
</b:includable>
<b:includable id='comment_picker' var='post'>
  <b:if cond='data:post.commentSource == 1'>
    <b:include data='post' name='iframe_comments'/>
    <b:else/>
    <b:if cond='data:post.showThreadedComments'>
      <b:include data='post' name='threaded_comments'/>
      <b:else/>
      <b:include data='post' name='comments'/>
    </b:if>
  </b:if>
</b:includable>
<b:includable id='comments' var='post'>
  <div class='comments' id='comments'>
    <a name='comments'/>
    <b:if cond='data:post.allowComments'>
      <h4>
        <data:post.commentLabelFull/>
        :
      </h4>
      <b:if cond='data:post.commentPagingRequired'>
        <span class='paging-control-container'>
          <b:if cond='data:post.hasOlderLinks'>
            <a expr:class='data:post.oldLinkClass' expr:href='data:post.oldestLinkUrl'>
              <data:post.oldestLinkText/>
            </a>
            &#160;
            <a expr:class='data:post.oldLinkClass' expr:href='data:post.olderLinkUrl'>
              <data:post.olderLinkText/>
            </a>
            &#160;
          </b:if>
          <data:post.commentRangeText/>
          <b:if cond='data:post.hasNewerLinks'>
            &#160;
            <a expr:class='data:post.newLinkClass' expr:href='data:post.newerLinkUrl'>
              <data:post.newerLinkText/>
            </a>
            &#160;
            <a expr:class='data:post.newLinkClass' expr:href='data:post.newestLinkUrl'>
              <data:post.newestLinkText/>
            </a>
          </b:if>
        </span>
      </b:if>
      <div expr:id='data:widget.instanceId + &quot;_comments-block-wrapper&quot;'>
        <dl expr:class='data:post.avatarIndentClass' id='comments-block'>
          <b:loop values='data:post.comments' var='comment'>
            <dt expr:class='&quot;comment-author &quot; + data:comment.authorClass' expr:id='data:comment.anchorName'>
              <b:if cond='data:comment.favicon'>
                <img expr:src='data:comment.favicon' height='16px' style='margin-bottom:-2px;' width='16px'/>
              </b:if>
              <b:if cond='data:blog.enabledCommentProfileImages'>
                <data:comment.authorAvatarImage/>
              </b:if>
              <data:comment.author/>
              <data:commentPostedByMsg/>
            </dt>
          </b:loop>
        </dl>
      </div>
      <b:if cond='data:post.commentPagingRequired'>
        <span class='paging-control-container'>
          <a expr:class='data:post.oldLinkClass' expr:href='data:post.oldestLinkUrl'>
            <data:post.oldestLinkText/>
          </a>
          <a expr:class='data:post.oldLinkClass' expr:href='data:post.olderLinkUrl'>
            <data:post.olderLinkText/>
          </a>
          &#160;
          <data:post.commentRangeText/>
          &#160;
          <a expr:class='data:post.newLinkClass' expr:href='data:post.newerLinkUrl'>
            <data:post.newerLinkText/>
          </a>
          <a expr:class='data:post.newLinkClass' expr:href='data:post.newestLinkUrl'>
            <data:post.newestLinkText/>
          </a>
        </span>
      </b:if>
      <p class='comment-footer'>
        <b:if cond='data:post.embedCommentForm'>
          <b:if cond='data:post.allowNewComments'>
            <b:include data='post' name='comment-form'/>
            <b:else/>
            <data:post.noNewCommentsText/>
          </b:if>
          <b:else/>
          <b:if cond='data:post.allowComments'>
            <a expr:href='data:post.addCommentUrl' expr:onclick='data:post.addCommentOnclick'>
              <data:postCommentMsg/>
            </a>
          </b:if>
        </b:if>
      </p>
    </b:if>
    <b:if cond='data:showCmtPopup'>
      <div id='comment-popup'>
      </div>
    </b:if>
    <div id='backlinks-container'>
      <div expr:id='data:widget.instanceId + &quot;_backlinks-container&quot;'>
        <b:if cond='data:post.showBacklinks'>
          <b:include data='post' name='backlinks'/>
        </b:if>
      </div>
    </div>
  </div>
</b:includable>

<b:includable id='threaded-comment-form' var='post'>
  <div class='comment-form'>
    <a name='comment-form'/>
    <b:if cond='data:mobile'>
      <p>
        <data:blogCommentMessage/>
      </p>
      <data:blogTeamBlogMessage/>
      <a expr:href='data:post.commentFormIframeSrc' id='comment-editor-src'/>
      <iframe allowtransparency='true' class='blogger-iframe-colorize blogger-comment-from-post' frameborder='0' height='410' id='comment-editor' name='comment-editor' src='' style='display: none' width='100%'/>
      <b:else/>
      <p>
        <data:blogCommentMessage/>
      </p>
      <data:blogTeamBlogMessage/>
      <a expr:href='data:post.commentFormIframeSrc' id='comment-editor-src'/>
      <iframe allowtransparency='true' class='blogger-iframe-colorize blogger-comment-from-post' frameborder='0' height='410' id='comment-editor' name='comment-editor' src='' width='100%'/>
    </b:if>
    <data:post.friendConnectJs/>
    <data:post.cmtfpIframe/>
    <script type='text/javascript'>
      BLOG_CMT_createIframe(&#39;<data:post.appRpcRelayPath/>&#39;);
    </script>
  </div>
</b:includable>
<b:includable id='threaded_comment_js' var='post'>
  <script async='async' expr:src='data:post.commentSrc' type='text/javascript'/>
  <script type='text/javascript'>
    (function() {
      var items = <data:post.commentJso/>;
      var msgs = <data:post.commentMsgs/>;
      var config = <data:post.commentConfig/>;
      // <![CDATA[
      var cursor = null;
      if (items && items.length > 0) {
        cursor = parseInt(items[items.length - 1].timestamp) + 1;
      }
      var bodyFromEntry = function(entry) {
        if (entry.gd$extendedProperty) {
          for (var k in entry.gd$extendedProperty) {
            if (entry.gd$extendedProperty[k].name == 'blogger.contentRemoved') {
              return '<span class="deleted-comment">' + entry.content.$t + '</span>';
            }
          }
        }
        return entry.content.$t;
      }
      var parse = function(data) {
        cursor = null;
        var comments = [];
        if (data && data.feed && data.feed.entry) {
          for (var i = 0, entry; entry = data.feed.entry[i]; i++) {
            var comment = {};
            // comment ID, parsed out of the original id format
            var id = /blog-(\d+).post-(\d+)/.exec(entry.id.$t);
            comment.id = id ? id[2] : null;
            comment.body = bodyFromEntry(entry);
            comment.timestamp = Date.parse(entry.published.$t) + '';
            if (entry.author && entry.author.constructor === Array) {
              var auth = entry.author[0];
              if (auth) {
                comment.author = {
                  name: (auth.name ? auth.name.$t : undefined),
                  profileUrl: (auth.uri ? auth.uri.$t : undefined),
                  avatarUrl: (auth.gd$image ? auth.gd$image.src : undefined)
                };
              }
            }
            if (entry.link) {
              if (entry.link[2]) {
                comment.link = comment.permalink = entry.link[2].href;
              }
              if (entry.link[3]) {
                var pid = /.*comments\/default\/(\d+)\?.*/.exec(entry.link[3].href);
                if (pid && pid[1]) {
                  comment.parentId = pid[1];
                }
              }
            }
            comment.deleteclass = 'item-control blog-admin';
            if (entry.gd$extendedProperty) {
              for (var k in entry.gd$extendedProperty) {
                if (entry.gd$extendedProperty[k].name == 'blogger.itemClass') {
                  comment.deleteclass += ' ' + entry.gd$extendedProperty[k].value;
                } else if (entry.gd$extendedProperty[k].name == 'blogger.displayTime') {
                  comment.displayTime = entry.gd$extendedProperty[k].value;
                }
              }
            }
            comments.push(comment);
          }
        }
        return comments;
      };
      var paginator = function(callback) {
        if (hasMore()) {
          var url = config.feed + '?alt=json&v=2&orderby=published&reverse=false&max-results=50';
          if (cursor) {
            url += '&published-min=' + new Date(cursor).toISOString();
          }
          window.bloggercomments = function(data) {
            var parsed = parse(data);
            cursor = parsed.length < 50 ? null
            : parseInt(parsed[parsed.length - 1].timestamp) + 1
            callback(parsed);
            window.bloggercomments = null;
          }
          url += '&callback=bloggercomments';
          var script = document.createElement('script');
          script.type = 'text/javascript';
          script.src = url;
          document.getElementsByTagName('head')[0].appendChild(script);
        }
      };
      var hasMore = function() {
        return !!cursor;
      };
      var getMeta = function(key, comment) {
        if ('iswriter' == key) {
          var matches = !!comment.author
          && comment.author.name == config.authorName
          && comment.author.profileUrl == config.authorUrl;
          return matches ? 'true' : '';
        } else if ('deletelink' == key) {
          return config.baseUri + '/delete-comment.g?blogID='
          + config.blogId + '&postID=' + comment.id;
        } else if ('deleteclass' == key) {
          return comment.deleteclass;
        }
        return '';
      };
      var replybox = null;
      var replyUrlParts = null;
      var replyParent = undefined;
      var onReply = function(commentId, domId) {
        if (replybox == null) {
          // lazily cache replybox, and adjust to suit this style:
          replybox = document.getElementById('comment-editor');
          if (replybox != null) {
            replybox.height = '250px';
            replybox.style.display = 'block';
            replyUrlParts = replybox.src.split('#');
          }
        }
        if (replybox && (commentId !== replyParent)) {
          document.getElementById(domId).insertBefore(replybox, null);
          replybox.src = replyUrlParts[0]
          + (commentId ? '&parentID=' + commentId : '')
          + '#' + replyUrlParts[1];
          replyParent = commentId;
        }
      };
      var hash = (window.location.hash || '#').substring(1);
      var startThread, targetComment;
      if (/^comment-form_/.test(hash)) {
        startThread = hash.substring('comment-form_'.length);
      } else if (/^c[0-9]+$/.test(hash)) {
        targetComment = hash.substring(1);
      }
      // Configure commenting API:
      var configJso = {
        'maxDepth': config.maxThreadDepth
      };
      var provider = {
        'id': config.postId,
        'data': items,
        'loadNext': paginator,
        'hasMore': hasMore,
        'getMeta': getMeta,
        'onReply': onReply,
        'rendered': true,
        'initComment': targetComment,
        'initReplyThread': startThread,
        'config': configJso,
        'messages': msgs
      };
      var render = function() {
        if (window.goog && window.goog.comments) {
          var holder = document.getElementById('comment-holder');
          window.goog.comments.render(holder, provider);
        }
      };
      // render now, or queue to render when library loads:
      if (window.goog && window.goog.comments) {
        render();
      } else {
        window.goog = window.goog || {};
        window.goog.comments = window.goog.comments || {};
        window.goog.comments.loadQueue = window.goog.comments.loadQueue || [];
        window.goog.comments.loadQueue.push(render);
      }
    })();
    // ]]>
  </script>
</b:includable>
<b:includable id='threaded_comments' var='post'>
  <div class='comments' id='comments'>
    <a name='comments'/>
    <h4>
      <data:post.commentLabelFull/>
      :
    </h4>
    <div class='comments-content'>
      <b:if cond='data:post.embedCommentForm'>
        <b:include data='post' name='threaded_comment_js'/>
      </b:if>
      <div id='comment-holder'>
        <data:post.commentHtml/>
      </div>
    </div>
    <p class='comment-footer'>
      <b:if cond='data:post.allowNewComments'>
        <b:include data='post' name='threaded-comment-form'/>
        <b:else/>
        <data:post.noNewCommentsText/>
      </b:if>
    </p>
    <b:if cond='data:showCmtPopup'>
      <div id='comment-popup'>
        <iframe allowtransparency='true' frameborder='0' id='comment-actions' name='comment-actions' scrolling='no'>
        </iframe>
      </div>
    </b:if>
    <div id='backlinks-container'>
      <div expr:id='data:widget.instanceId + &quot;_backlinks-container&quot;'>
        <b:if cond='data:post.showBacklinks'>
          <b:include data='post' name='backlinks'/>
        </b:if>
      </div>
    </div>
  </div>
</b:includable>
<!-- コメントの構成コード終わり -->

上記がコメントを保存できるようにするコードの全文である。

これを <b:includable id='post-comments'> ~ </b:includable> の下辺りに記述しておけばよい。厳密にいえば、メインセクションのブログ投稿ガジェット内に記述すればOKだと思う。

CSS


Bloggerテンプレート自作 #14:コメントフォームを設置する
埋め込み
Bloggerテンプレート自作 #14:コメントフォームを設置する
ポップアップ

/* ----------------------------------------------------------------------------
    コメント
---------------------------------------------------------------------------- */

.comment-wrap {
  margin-top: 20px;
}

.comment-wrap h3 {
  font-size: 18px;
  letter-spacing: 2px;
  background: #333;
  color: #fff;
  padding: 6px 15px;
  margin: 0;
}

.comment-area {
  background: #fff;
  padding: 10px 20px;
}

.comment-area a {
  font-size: 13px;
  text-decoration: none;
  color: #00f;
  padding: 2px;
}

.comment-area a:hover {
  opacity: 0.6;
}

.comment-area h4 {
  display: inline;
  font-size: 15px;
  color: #555;
  padding: 2px 0;
  margin-left: 8px;
  border-bottom: dashed 1px #555;
}

.comment-area p {
  font-size: 13px;
}

/* 埋め込み時
---------------------------------------------------------------------------- */

.comments .comment-block {
  font-size: 14px;
  margin-left: 42px;
}

.comments .comments-content .user {
  font-size: 14px;
  color: #555;
}

.comments .comments-content .user a {
  font-size: 14px;
}

.datetime a {
  color: #666;
}

.continue {
  font-size: 13px;
}

.comments .comments-content .comment {
  margin-bottom: 5px;
}

/* 返信の背景 */

.comment-replies {
  background-color: #fafafa;
}

.comment-content {
  padding-right: 5%;
}

/* ポップアップ時
---------------------------------------------------------------------------- */

.comment-author {
  font-size: 13px;
  color: #666;
  font-weight: normal;
}

.comment-footer {
  margin: 10px;
}

.comment-footer a {
  display: block;
  color: #333;
  background: #eee;
  font-weight: bold;
  text-align: center;
  padding: 10px 0;
}

/* レスポンシブ(スマホ縦) */

@media screen and (max-width: 560px) {
  .comment-area {
    padding: 10px;
  }
  .comment-area h4 {
    font-size: 13px;
    margin-left: 0;
  }
}

とりあえず、上記のコードで画像のようなデザインになる。

余談


エディタの自動補完機能を使うとコードが効かなくなる?!


結論から言えば "「コメントを保存できるようにするコード」を記述したファイルをフォーマッタでキレイに整えると、コードが書き換えられてコメントが使えなくなる" という不具合が起きるので注意ということ。

自分はVS Codeというプログラミング用のエディタでテンプレートを作成しているのだが、このエディタにはフォーマッタというコードのインデントを自動で整えてくれる便利機能がある。しかし、これはコードの誤記入の補完機能も兼ねているため、エラーと判断されたら、該当部分が正しいとされるコードに変換されてしまう。

そもそもVS CodeはBloggerテンプレートには非対応なのだが、テンプレートタグは独自のタグと解釈されてHTMLのフォーマッタでキレイに整形できるという裏技がある。しかし、今回の「コメントを保存できるようにするコード」を入れてHTMLのフォーマッタを使うと、コメントが効かなくなった。原因は上記の通り、コードの自動補完によるものだろう。

で、具体的にどのようなエラーが起きるのかと言うと「テンプレートをアップロードする際に"記述にエラーがあるので読み込めない"という旨のエラーが出てアップロード不能になる(VS Codeで確認)」あるいは「テンプレートのアップロードはできるが、コメント投稿時に投稿を確認するダイアログが表示されずにコメントできなくなる(Atomで確認)」というもの。もし、上記のコードを使ってテンプレートが読み込めなくなった場合は、エディタの自動補完機能を疑うべし。

ちなみに具体的には、自動インデント時に特殊文字混じりのidやclass名に自動でホワイトスペースが入り、コメントのJSがidやclass名を取得できなくなることが原因な模様。

コメント入力フォームのカスタマイズについて


コメント入力フォーム(コメントの入力や公開がコントロールできる部分)はJavaScriptで自動生成されるものなので、CSSで色や幅などを設定することはできないっぽい。もしできるとしたらJavaScriptで記述された部分を変えるしか無いと思う。ってか実際にデザインを変えている人がいるけど、多分JSでやっているんだろうな~。