|||||||||||||||||||||

なんぶ電子

- 更新: 

Bloggerで関連記事やサイトマップを作成

#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();

筆者紹介


自分の写真
がーふぁ、とか、ふぃんてっく、とか世の中すっかりハイテクになってしまいました。プログラムのコーディングに触れることもある筆者ですが、自分の作業は硯と筆で文字をかいているみたいな古臭いものだと思っています。 今やこんな風にブログを書くことすらAIにとって代わられそうなほど技術は進んでいます。 生活やビジネスでPCを活用しようとするとき、そんな第一線の技術と比べてしまうとやる気が失せてしまいがちですが、おいしいお惣菜をネットで注文できる時代でも、手作りの味はすたれていません。 提示されたもの(アプリ)に自分を合わせるのでなく、自分の活動にあったアプリを作る。それがPC活用の基本なんじゃなかと思います。 そんな意見に同調していただける方向けにLinuxのDebianOSをはじめとした基本無料のアプリの使い方を紹介できたらなと考えています。

広告