やっと完成!はてなブログの記事下にサムネイル付きの関連記事をランダムに出す方法(2015 11/5,11/19,12/8追記)
1度作ってからバグ修正などを何度か繰り返してきましたが、再度大幅に修正しなければいけない部分が出てきたので、作り直しました。
※2015 11/15 追記
と言いつつまたもや修正してしまいました。
このブログは問題ないのですが、TOPページに複数記事表示しているブログを想定していないコードなので、TOPページに複数記事表示している場合は表示しないようにしました。
更に簡単な解説記事も書いたので、そちらも読んでください。
※2015 11/19 追記
またもや修正しました。
最近記事が増えてきたので検索ボックスを表示するように変更したのですが、今までのコードでは検索結果の画面にカテゴリ一覧が表示されてしまうので、URLの判定を追加しました。
PC版の見た目
スマホ版の見た目
ある日関連記事が出力されなくなっていた
記事を書いていて、はてなブログのプレビュー機能で確認していて気が付いたのですが、ある日急に関連コンテンツが出てこなくなっていました。
テンプレート出力部分で利用していたはてなブログのjsの仕様が変わったらしい
調査したところ、元のプログラムではHTMLの生成部分で、はてなブログのjsの「_.template()」を利用していたのですが、この関数の仕様が変わったらしくエラーが出ていました。
やっぱり外部の仕組みに依存するのはよくないなーと思い、jqueryでHTMLを独自に組み立てることにしました。
ついでに追加した要素
どうせ修正するならということで、何点か追加で機能を実装しました。
- プロフィールページと404エラーページにに主要カテゴリの関連コンテンツを表示
- 関連コンテンツの表示順をランダムに変更
- 幅の広いブラウザの場合に本文の最初の150文字を表示
404エラー時に主要カテゴリの関連コンテンツを表示
表示している記事のカテゴリを取得しているのですが、404の場合にカテゴリを取得できないので、むりやりこのブログの主要カテゴリと思っている「自転車屋」「お酒」「はてブロ」の記事を表示することにしました。
関連コンテンツの表示順をランダムに変更
元々は表示している記事と同じカテゴリページの上から指定した数の記事を表示していました。
この並び順だと、結局同じカテゴリの最新の記事を出しているということになり、あまり古い記事の掘り起こしになりません。
そのため、カテゴリページの記事をランダムに指定した数だけ表示することにしました。
幅の広いブラウザの場合に本文の最初の150文字を表示
PCの場合にタイトルだけだと余白がもったいないので、概要文代わりに本文の最初の方だけ表示することにしました。
はまった部分
目的自体は明確だったので、それほど実装は難しくなかったのですが、はまった部分がありました。
概要文を出力すると関連コンテンツが崩れる
概要文を出力すると関連コンテンツが時々崩れてしまいました。
※ランダム表示していたりして、原因がなかなか分かりませんでした。
原因は概要文として出力している本文の中にHTMLのタグが混ざっていて、そのタグと関連コンテンツのHTMLが干渉して崩れていました。
こちらの解消するにはHTMLのタグをエスケープ必要があるのですが、jQueryにはHTMLのエスケープ処理が用意されておらず、よく事例で出てくる下のような書き方は抜け漏れがあるとのことなので、面倒ですが参考記事の方法で対応しました。
※事例として多いが、対応に漏れがある方法
$('<div />').text(content).html();
※事例のURL
javascript
フッターに書くjavascriptはこんな感じです。
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script>
$(document).ready(function(){
/*関連コンテンツの制御*/
var path = location.pathname;//2015 12/8 途中の修正でコードが抜けていました。
//2015 11/5 , 11/19 修正
//カテゴリ一覧と検索結果とTOPページに複数記事の場合は処理を行わない
if(path.indexOf("/archive") < 0 && path.indexOf("/search") < 0 && $('article').length <= 1 ){
setTimeout(function(){
/* 関連コンテンツ出力 */
var maxEntries = 3;
var categorytags = [];
var category = $('.categories');
if (category.size()) {
var categoryobjs = category.find('a');
var categoryobjscount = categoryobjs.length;
for(var i=0 ; i < categoryobjscount; i++){
//パンくずのTOPを除外
if(categoryobjs.eq(i).text() != "トップ"){
categorytags.push(categoryobjs.eq(i).text());
}
}
//404エラー+プロフィール画面の場合
}else{
categorytags = ['自転車屋','お酒','はてブロ'];
}
var origin = location.protocol + '//' + location.host;
var startFetchCategoryEntries = function (d, categories) {
d = d || $.Deferred();
categories = categories || [];
var categoryTag = categorytags.shift();
if (!categoryTag) {
d.resolve(categories);
return d;
}
var category = {
name: categoryTag,
entries: []
};
//カテゴリページを取得しタイトルとサムネイル画像とURLと概要文を取得
$.ajax(origin + '/archive/category/' + encodeURIComponent(category.name)).done(function (feed) {
var sectionobj = $(feed).find('section');
var sectioncount = sectionobj.length -1;
//ランダムに記事の番号を選ぶ
var sectionNolist =[];
for(var ii=0 ; ii < 20; ii++){
randomNumber = Math.floor( Math.random() * (sectioncount + 1) );
if($.inArray(randomNumber, sectionNolist) == -1){
sectionNolist.push(randomNumber);
}
}
var sectionNolistcount = sectionNolist.length;
for(var ii=0 ; ii < sectionNolistcount; ii++){
var $feedEntry = sectionobj.eq(sectionNolist[ii]);
var entry = $feedEntry.find('h1').find('a');
var thum = $feedEntry.find('.entry-thumb').css('background-image');
var thum2 = "";
if(thum){
if( thum.match( /^url\(["']?(.*?)["']?\)/i )[1] != null ) {
thum2 = thum.match( /^url\(["']?(.*?)["']?\)/i )[1];
}
}
var desc = $feedEntry.find('.entry-description');
var summary = "";
if(desc.text().length >0){
var summary = desc.text().substr(0,150)+"...";
summary = escapeHtml(summary);
}
var entry = {
title: entry.text(),
url: entry.attr('href'),
thumnailurl: thum2,
summary: summary,
published: new Date($feedEntry.find('time').attr('datetime'))
};
thiscontentstitle = $('.entry-title').find('a').attr('href');
if (entry.url === thiscontentstitle) continue;
entry.url = entry.url + "?recommend=" + location.pathname;
category.entries.push(entry);
if (category.entries.length >= maxEntries && maxEntries !== -1) {
break;
}
};
categories.push(category);
startFetchCategoryEntries(d, categories);
}).fail(function (xhr) {
(console.error || console.log).call(console, xhr);
});
return d;
}
//関連コンテンツのhtml生成
startFetchCategoryEntries().done(function (categories) {
html = '<div class="same-category-entries">';
var categoriescount = categories.length;
var issmp = false;
if ($(window).width() < 480) {
var issmp = true;
}
for (var i=0; i< categoriescount; i++) {
categorydata = categories[i];
if (categorydata.entries.length > 0) {
html = html + '<div class="same-category-entries-category">'
+'<h2 class="same-category-entries-category-title"><a class="same-category-entries-category-name" href="/category/' + categorydata.name + ' ">' + categorydata.name + '</a>カテゴリの他の記事</h2>'
+'<ul class="same-category-entries-list">';
var categorydataentriescount = categorydata.entries.length;
for (var ii=0; ii< categorydataentriescount; ii++) {
entry = categorydata.entries[ii];
html = html + '<li class="same-category-entries-entry"><a href="'+entry.url +'"><p class="same-category-entries-entry-thum" >';
if (entry.thumnailurl.length > 0) {
html = html + '<img src ="'+ entry.thumnailurl +'"></p><p class="same-category-entries-entry-title" >'+ entry.title +'</p>';
}else{
html = html + '</p><p class="same-category-entries-entry-title" >'+ entry.title +'</p>';
}
if(!issmp){
html = html + '<p class="same-category-entries-entry-summary" >'+ entry.summary +'</p>';
}
publishedYear = entry.published.getFullYear();
publishedMonth = entry.published.getMonth()+1;
publishedDate = entry.published.getDate();
html = html + '<div class="same-category-entries-entry-date">'+ publishedYear +'/'+ publishedMonth +'/'+ publishedDate +'</div></a></li>';
}
html = html + '</ul></div>';
}
}
html = html + '</div>';
var $html = $($.parseHTML(html));
$(".entry-content").after($html);
});
},1000);
}
});
var str = '& < > ` " ' + "'";
var escapeHtml = (function (String) {
var escapeMap = {
'&': '&',
"'": ''',
'`': '`',
'"': '"',
'<': '<',
'>': '>'
};
var escapeReg = '[';
var reg;
for (var p in escapeMap) {
if (escapeMap.hasOwnProperty(p)) {
escapeReg += p;
}
}
escapeReg += ']';
reg = new RegExp(escapeReg, 'g');
return function escapeHtml (str) {
str = (str === null || str === undefined) ? '' : '' + str;
return str.replace(reg, function (match) {
return escapeMap[match];
});
};
}(String));
</script>
※関連コンテンツの表示先はこのブログ用に調整してあるので、調整してください。
$(".entry-content").after($html); の部分
CSS
見た目のCSSはこんな感じです。
※CSSはこのブログ用に調整してあるので、調整してください。
.same-category-entry-summary {
color: #666;
}
a.same-category-entries-category-name, a.same-category-entries-category-name:visited {
color: inherit;
}
.same-category-entries-category-name:before {
content: "【";
}
.same-category-entries-category-name:after {
content: "】";
}
.same-category-entry-meta {
color: #CCC;
margin-top: 0.5em;
}
.same-category-entries-category-title{
border-bottom: 1px solid #000;
margin-bottom:5px;
}
.same-category-entries-list{
list-style: outside none none;
padding-left: 0;
margin-bottom:0;
}
.same-category-entries-entry {
border-bottom: 0.1em solid #000;
padding-left: 0;
margin: 1em 0em;
}
.same-category-entries-entry-thum{
float:left;
margin-right: 10px;
}
.same-category-entries-entry-thum img{
width: 120px;
height: 120px;
border-radius: 5px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border: 1px #CDCBC6 solid;
}
.same-category-entries-entry a{
color: #333333;
position: relative;
display: inline-block;
}
.same-category-entries-entry a:hover{
color:#969696;
transition: .3s ease;
}
.same-category-entries-entry-title{
margin: 0;
font-weight:bold;
}
.same-category-entries-entry-summary{
margin: 0;
}
.same-category-entries-entry-date{
color: #696969;
}
やっと満足できるものになりました!!!
と言いつつまたいじるかも・・・。
※過去記事