Bloggerで関連記事やサイトマップを作成
Bloggerではガジェットとして「最新の記事」や「人気の記事」がありますが、「関連記事」が存在しません。なので今回は関連記事を表示するスクリプトを作成してみたいと思います。
表示するページが所属する「ラベル」を使って、同じラベルの記事をフィードから取得し、「関連記事」として表示してみようと思います。フィードの取得限界は500件という事なので、表示できる件数も通常の機能に比べて多く表示させることが可能です。
関連記事だけでなく、HTMLサイトマップなどにも応用できます。
ページからラベル名を取得できるように設定する
まず表示中のページが所属するラベルの名称をJavaScript経由で取得できるようにします。標準として用意されているテーマの中に取得できるような形でラベル名が存在すれば問題ありませんが、そうでなければテンプレートの構造を変更しておく必要があります。
「ブログの投稿」ガジェット内で「data:post.labels」とすることで、記事が所属するラベルのリストを取得できます。たとえば、それをb:loopで回しながらラベルを取得します。たとえば一番最初に出てきたラベルをidにlabel-nameを設定したspanタグとして出力します。これによりページ表示時にgetElementByIdからラベルが取得できるようになります。
<b:loop index='ilabelloop' values='data:post.labels' var='label'>
<b:if cond='data:ilabelloop==0'>
<span id="label-name">data:label.name</span>
</b:if>
</b:loop>
筆者の場合は、「パンくずリスト」でラベルを出力するようにしていますので、そこから取得しました。JavaScriptのコードは次のようになっています。
const getLabelName = () => {
try {
let step1 = document.getElementsByClassName('breadcrumbs')[0];
let step2 = step1.getElementsByTagName('li')[1];
let step3 = step2.getElementsByTagName('a')[0];
return step3.innerHTML;
} catch(e) {
//console.log(e);
return "";
}
}
フィードの取得
ターゲットとなるラベルが取得できるようになりましたので、今度はラベル別のフィードを取得します。クリボウの Blogger 入門によると、ラベル毎のフィードは「https://ブログページ/feeds/posts/default/-/ラベル」で取得できるそうです。
また、defaultまでで止めるとラベルの制限はなくなります。サイトマップの場合はこちらの方が便利かもしれません。ただし、最大500件までとなるそうです。
XMLHttpRequestを使ってXMLを取得します。この時、日本語のラベル名の場合はURLエンコードしないといけません。
const getFeed =strFeedUrl=> {
let req = new XMLHttpRequest();
let strResult="";
req.onreadystatechange=function(){
if (req.readyState == 4 && req.status == 200) {
strResult = req.response;
}
}
//falseを指定して結果を待つ
req.open('GET',strFeedUrl,false);
req.send(null);
return strResult;
}
let strTargetLabel=getLabelName();
let strXml = getFeed('https://nanbu.marune205.net/feeds/posts/default/-/'+encodeURI(strTargetLabel));
console.log(strXml);
XMLデータのparse
受け取ったXMLを解析(parse)します。
BloggerのラベルフィードではデフォルトではATOM形式になっているようです。feed要素の中のentryがひとつの記事に対応します。
entryの中にある関連記事に使えそうな要素は次の通りです。
- id
記事のidです
- published
投稿日時です
- updated
更新日時です
- category
category要素のterm属性にラベル名が入っています。
- title
記事のタイトルです
- summary
記事の先頭部分が入っています。htmlタグは除去されていて、かわりに改行コード(¥r¥n)が入っています。
- link
リンク要素は複数ありますが、このrel属性の値がalternateになっているものの、href要素に記事のURLがあります。
- media:thumbnail
この要素のurl属性値に、記事の先頭画像の縮小版が入っています(72px)
ちなみに取得時のURLにクエリパラメータ「?alt=json」を付与するとjsonでもデータを取得できるようですがparseがうまくいきませんでした。
XMLをJavaScriptのgetElementsByTagNameと、querySelectorを使ってparseします。entryはgetElementsByTagName、各値はquerySelectorを使いますが、「media:thumbnail」要素だけはquerySelectorでは捕まえられなかったので、getElementsByTagNameに置き換えています。
ここで取得した値を使って、要素を作成し、最後に目的の場所に挿入すれば完成です。
const parseFeed = strSrc => {
try {
let srcdiv = document.createElement('div');
let resultdiv = document.createElement('div');//実際にページに表示する要素
srcdiv.innerHTML=strSrc;
let entries = srcdiv.getElementsByTagName('entry');
for (let i =0; i < entries.length; i++) {
//タイトル
console.log(entries[i].querySelector('title').innerHTML);
//記事
console.log(entries[i].querySelector('summary').innerHTML);
//URL
console.log(entries[i].querySelector('link[rel="alternate"]').href);
//縮小画像リンク
console.log(entries[i].getElementsByTagName('media:thumbnail')[0].getAttribute('url'));
}
return resultdiv;
} catch(e) {
console.log(e);
return resultdiv;
}
}
参考になるかわかりませんが、筆者のページ使っているコードは次のようになります。先のコードではXMLHttpRequestを待って戻り値を得ていましたが、待たない方法で書いています。
ラベルの他投稿年月を取り、その直前の5件を取得するようにしています。
const getFeed =strFeedUrl=> {
//フィードを取得
let req = new XMLHttpRequest();
req.onreadystatechange=function(){
if (req.readyState == 4 && req.status == 200) {
let obj = parseFeed(req.response);
if (obj != null) {
document.getElementsByClassName('org-post')[0].appendChild(obj);
}
}
}
req.open('GET',strFeedUrl,true);
req.send(null);
}
const getLabelName = () => {
//記事のラベルを取得
try {
let step1 = document.getElementsByClassName('breadcrumbs')[0];
let step2 = step1.getElementsByTagName('li')[1];
let step3 = step2.getElementsByTagName('a')[0];
return step3.innerHTML;
} catch(e) {
//console.log(e);
return "";
}
}
const getYmd = () => {
//記事の年月日を取得
try {
let step1 = document.getElementsByClassName('org-date-header')[0];
let step2 = step1.getElementsByTagName('span')[0];
return step2.innerHTML.replace(/¥//g,'-');
} catch(e) {
//console.log(e);
return "";
}
}
const parseFeed = strSrc => {
try {
let srcdiv = document.createElement('div');
let resultdiv = document.createElement('div');//実際にページに表示する要素
let h2 = document.createElement('h2');
h2.classList.add('widgettitle');
h2.innerHTML='関連記事';
resultdiv.appendChild(h2);
let hrT = document.createElement('hr');
hrT.classList.add('defaulthr');
resultdiv.appendChild(hrT);
let ol1 = document.createElement('ol');
let ol2 = document.createElement('ol');
ol2.style='clear: both;';
//parse
srcdiv.innerHTML=strSrc;
let strRealUrl = location.href;
let intCnt = 0;
let entries = srcdiv.getElementsByTagName('entry');
for (let i =0; i < entries.length; i++) {
let strImgUrl = entries[i].getElementsByTagName('media:thumbnail')[0].getAttribute('url');
let strTitle = entries[i].querySelector('title').innerHTML;
let strPageUrl = entries[i].querySelector('link[rel="alternate"]').href;
if (0<=strRealUrl.indexOf(strPageUrl)) {
//表示中の記事は対象にしない
continue;
}
intCnt++;
if (6 <= intCnt) {
//表示件数は5
break;
}
//画像の処理
let a1 = document.createElement('a');
a1.href=strPageUrl;
a1.target='_blank';
a1.rel='noopener';
let ampimg = document.createElement('amp-img');
ampimg.setAttribute('alt',strTitle);
ampimg.setAttribute('height','72');
ampimg.setAttribute('width','72');
ampimg.setAttribute('layout','fixed');
ampimg.setAttribute('src',strImgUrl);
ampimg.setAttribute('style','border: solid; border-width: 0.5px;');
let noscript = document.createElement('noscript');
let img = document.createElement('img');
img.alt=strTitle;
img.height='72';
img.width='72';
img.src=strImgUrl;
img.style='border: solid; border-width: 0.5px;';
noscript.appendChild(img);
ampimg.appendChild(noscript);
a1.appendChild(ampimg);
let li1 = document.createElement('li');
li1.style='float:left;padding-right:1.5em;';
li1.appendChild(a1);
ol1.appendChild(li1);
//タイトルの処理
let li2 = document.createElement('li');
li2.classList.add('normal-weight');
let a2 = document.createElement('a');
a2.href=strPageUrl;
a2.target='_blank';
a2.rel='noopener';
a2.innerHTML=strTitle;
li2.appendChild(a2);
ol2.appendChild(li2);
}
resultdiv.appendChild(ol1);
resultdiv.appendChild(ol2);
return resultdiv;
} catch(e) {
console.log(e);
return null;
}
}
const getRelativePost = async () => {
let strLabelName = getLabelName();
let strYmd = getYmd();
if (strLabelName==="" || strYmd==="") {
return;
}
getFeed('https://nanbu.marune205.net/feeds/posts/default/-/'+encodeURI(strLabelName)+'?orderby=published&max-results=6&published-max='+strYmd+'T23%3A59%3A59%2B09%3A00');
}
getRelativePost();