Bloggerのフィードからブログカードを一気に作成(2)ーJavaScriptで実装ー

2021年6月3日木曜日

Blogger Bloggerカスタマイズ CSS HTML JavaScript ブログカード

t f B! P L

ブログ内に関連記事やおすすめの記事などのリンクを入れる際に、画像、題名、要約をまとめてブログカードにすると見栄えがよいですよね。前回記事でHTML+CSSで作ったブログカードのテンプレートをもとに、フィードからJavaScriptで全記事のブログカードを作成してコピペできるようにします。

Bloggerのフィードを取得する

まずはBloggerの設定でフィードを取得できるようにしましょう。「設定」-「サイトフィード」-「ブログフィードを許可」で「完全」を選択します。

Bloggerで記事のフィードを取得するには、ブラウザに下記アドレスのうちいずれかを入力します。ブログIDは、Blogger管理画面にログインした際にURLに現れる数字の列です。

https://独自ドメイン名/feeds/posts/default
https://ブログ名.blogspot.com/feeds/posts/default
https://www.blogger.com/feeds/ブログID/posts/default

デフォルトでは数記事しか取得されません。published-min, published-maxパラメータで指定した期間の記事を取得できます。例えば、www.bchari.comから2018/9/16~2021/5/14までの記事を取得するには、ブラウザに下記アドレスを入力します。

https://www.bchari.com/feeds/posts/default?published-min=2018-09-16T00:00:00&published-max=2021-05-14T23:59:59&max-results=200

するとAtom1.0形式のフィードが表示されます。これをfeed.xmlなどの名前をつけて保存します。

<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:blogger='http://schemas.google.com/blogger/2008' xmlns:georss='http://www.georss.org/georss' xmlns:gd="http://schemas.google.com/g/2005" xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-6266051941055901697</id><updated>2021-06-03T06:55:50.331+09:00</updated><category term="自転車"/><category term="アウトドア"/><category term="DIY"/><category term="ルイガノ"/><category term="LGS-MV1"/><category term="ロードバイク"/><category term="MADONE 2.1"/><category term="TREK"/><category term="CAD ...

JavaScriptでフィードからブログカードを作成

ファイルを読み込むボタンと出力のフォームを準備

フォームを使い、ボタンを押すとファイルを読み込み、結果をテキストエリアに出力するためのHTMLです。

<form name="myform">
  <input name="myfile" type="file" /><br />
  <textarea name="result" rows="30" cols="80"></textarea>
</form>

ファイルの読み込み

フォームにファイルが選択されたときのイベントリスナを指定し、さらにファイルの読み込みが完了したところでイベントリスナを指定してイベントリスナの関数の中にファイル読み込み後の処理を書きます。

const form = document.forms.myform;
form.myfile.addEventListener("change", function (e) {
  let feedData = e.target.files[0];
  let reader = new FileReader();
  reader.readAsText(feedData);

  reader.addEventListener("load", function () {

  /* ファイル読み込み後の動作をこの中に書く */

  });
});

DOMParserで記事を取得

DOMParserを使い、ファイルの中身からDOMを構築します。各記事は<entry></entry>タグに囲まれているのでgetElementsByTagName("entry")で取得できます。


/* 記事(entryタグ)を全部取得 */
let parser = new DOMParser();
let feed = parser.parseFromString(this.result, "application/xml");
let entries = feed.getElementsByTagName("entry");

Entry内の必要な情報を配列に格納

Entry内のタイトル、URI、投稿日のデータを配列に入れます。

/* Entry内のタイトル、URI、投稿日のデータを配列に入れる */
let s = new XMLSerializer();
let feedItems = [];
for (let entry of entries) {
  /* entry から必要な情報をfeedItem[]配列に抜き出す */
  feedItem = [];
  feedItem["title"] = entry.querySelector("title").textContent;
  feedItem["uri"] = entry
    .querySelector("link[rel='alternate'")
    .getAttribute("href");
  feedItem["published"] = entry.querySelector("published").textContent;
}

記事の中身は実体参照文字をデコードしてからDOMParserを適用

記事の中身は、entry内の、contentタグの中に入っていますが、contentの中身はHTMLの特殊文字を実体参照文字に置き換えた状態になっています。例えばこんな感じです。

<content type='html'>&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt; &lt;img alt=&quot;title-image&quot; border=&quot;0&quot; data-original-height=&quot;360&quot; data-original-width=&quot;640&quot; height=&quot;180&quot; loading=&quot;lazy&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhmTwA1HnQ8Kqo1vI490BVCiwVql9nIeSi6OBvwDwAM1HQlgevWdqT7244UL_v3XjWyqB_y6QzrafHVv8A-K2-H9gmWp_oxsyy5JJj7BmCTnhsW8cD3GND-BPMwjcHaM6e46zHboCWwMmk/s320/shinkansen-park-00000.webp&quot; srcset=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhmTwA1HnQ8Kqo1vI490BVCiwVql9nIeSi6OBvwDwAM1HQlgevWdqT7244UL_v3XjWyqB_y6QzrafHVv8A-K2-H9gmWp_oxsyy5JJj7BmCTnhsW8cD3GND-BPMwjcHaM6e46zHboCWwMmk/s640/shinkansen-park-00000.webp 2x, https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhmTwA1HnQ8Kqo1vI490BVCiwVql9nIeSi6OBvwDwAM1HQlgevWdqT7244UL_v3XjWyqB_y6QzrafHVv8A-K2-H9gmWp_oxsyy5JJj7BmCTnhsW8cD3GND-BPMwjcHaM6e46zHboCWwMmk/s320/shinkansen-park-00000.webp 1x&quot; width=&quot;320&quot;/&gt;&lt;/div&gt;&lt;p&gt; Googleマップで発見した気になるスポット、新幹線公園に行ってみました。 ...

この実体参照文字をいったんデコードして通常のHTMLに変換した後、contentタグをDOMParserを適用してDOMツリーを取得する必要があります。デコードした後のHTMLはこうなります。

<content type='html'><div class="separator" style="clear: both; text-align: center;"> <img alt="title-image" border="0" data-original-height="360" data-original-width="640" height="180" loading="lazy" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhmTwA1HnQ8Kqo1vI490BVCiwVql9nIeSi6OBvwDwAM1HQlgevWdqT7244UL_v3XjWyqB_y6QzrafHVv8A-K2-H9gmWp_oxsyy5JJj7BmCTnhsW8cD3GND-BPMwjcHaM6e46zHboCWwMmk/s320/shinkansen-park-00000.webp" srcset="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhmTwA1HnQ8Kqo1vI490BVCiwVql9nIeSi6OBvwDwAM1HQlgevWdqT7244UL_v3XjWyqB_y6QzrafHVv8A-K2-H9gmWp_oxsyy5JJj7BmCTnhsW8cD3GND-BPMwjcHaM6e46zHboCWwMmk/s640/shinkansen-park-00000.webp 2x, https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhmTwA1HnQ8Kqo1vI490BVCiwVql9nIeSi6OBvwDwAM1HQlgevWdqT7244UL_v3XjWyqB_y6QzrafHVv8A-K2-H9gmWp_oxsyy5JJj7BmCTnhsW8cD3GND-BPMwjcHaM6e46zHboCWwMmk/s320/shinkansen-park-00000.webp 1x" width="320"/></div><p> Googleマップで発見した気になるスポット、新幹線公園に行ってみました。 ...

Javascriptでの実体参照文字のデコードは、ちょっとトリッキーですがフォームのテキストエリアにコピペするというテクニックがあるようです。JavaScript HTML Entities Encode & Decode

/**
 * HTML entities decode
 *
 * @param {string} str Input text
 * @return {string} Filtered text
 */
function htmldecode(str) {
  var txt = document.createElement("textarea");
  txt.innerHTML = str;
  return txt.value;
}

この関数を使いcontentタグ内をデコードし、記事内の一番最初に出てくる画像と、一番最初に出てくるpタグ内の文字列を取得しています。ブログカード用に画像を縮小する方法に関して細くです。Bloggerの画像にはURI内に/s320/などのサイズを表す文字列があり、この文字列を例えばs160とすると長辺160pxの画像にしてくれるので、URIの文字列置換をすることで画像縮小をしています。

pタグの内容は最初の50文字を取り出すようにしました。

/* entry内の記事中身(content)の実体参照文字をデコードする */
let content = htmldecode(s.serializeToString(entry.querySelector("content")));

/* DOMParserを適用 */
let contentDOM = parser.parseFromString(content, "text/html");
if (contentDOM.querySelector("img") != null) {
  let imgsrc = contentDOM.querySelector("img").getAttribute("src");
  feedItem["img"] = imgsrc.replace(/\/s\d{3,4}\//, "/s160/");
}
if (contentDOM.querySelector("p") != null) {
  feedItem["snippet"] = contentDOM.querySelector("p").textContent.substr(0,50);
}

これら抜き出した値をテンプレートに入れます。フォーマット文字列で置き換えます。

feedItem["card"] = `<article class="blog-card">
  <a href="${feedItem["uri"]}"  aria-label="${feedItem["title"]}">
    <img src="${feedItem["img"]}" width="160" height="90" alt="thumbnail" loading="lazy"/>
  </a>
  <div class="text-contents">
    <h3 class="title">
      <a href="${feedItem["uri"]}">
    ${feedItem["title"]}
      </a>
    </h3>
    <p>
      ${feedItem["snippet"]} ...
    </p>
  </div>
</article>
`;

テキストボックスに結果を書き込んで終了です。

form.result.textContent += feedItem["card"];

コード

最後にHTML, Javascript一式のコードを載せます。

<html>
  <body>
    <p>Atom形式のフィードからブログカードを作成</p>
    <form name="myform">
      <input name="myfile" type="file" /><br />
      <textarea name="result" rows="30" cols="80"></textarea>
    </form>
    <p id="blogCard"></p>
    <script>
      /**
       * HTML entities decode
       *
       * @param {string} str Input text
       * @return {string} Filtered text
       */
      function htmldecode(str) {
        var txt = document.createElement("textarea");
        txt.innerHTML = str;
        return txt.value;
      }

      /* Atomファイル(XML形式)を読み込み */
      const form = document.forms.myform;
      form.myfile.addEventListener("change", function (e) {
        let feedData = e.target.files[0];
        let reader = new FileReader();
        reader.readAsText(feedData);

        /* ファイル読み込み後の動作 */
        reader.addEventListener("load", function () {
          let blogCardParagraph = document.getElementById("blogCard");

          /* 記事(entryタグ)を全部取得 */
          let parser = new DOMParser();
          let feed = parser.parseFromString(this.result, "application/xml");
          let entries = feed.getElementsByTagName("entry");

          /* Entry内のタイトル、URI、投稿日のデータを配列に入れる */
          let s = new XMLSerializer();
          let feedItems = [];
          for (let entry of entries) {
            /* entry から必要な情報をfeedItem[]配列に抜き出す */
            feedItem = [];
            feedItem["title"] = entry.querySelector("title").textContent;
            feedItem["uri"] = entry
              .querySelector("link[rel='alternate'")
              .getAttribute("href");
            feedItem["published"] = entry.querySelector(
              "published"
            ).textContent;

            /* entry内の記事中身(content)の実体参照文字をデコードする */
            let content = htmldecode(
              s.serializeToString(entry.querySelector("content"))
            );

            /* DOMParserを適用 */
            let contentDOM = parser.parseFromString(content, "text/html");
            if (contentDOM.querySelector("img") != null) {
              let imgsrc = contentDOM.querySelector("img").getAttribute("src");
              feedItem["img"] = imgsrc.replace(/\/s\d{3,4}\//, "/s160/");
            }
            if (contentDOM.querySelector("p") != null) {
        feedItem["snippet"] = contentDOM.querySelector("p").textContent.substr(0,50);
            }

            feedItem["card"] = `<article class="blog-card">
  <a href="${feedItem["uri"]}"  aria-label="${feedItem["title"]}">
    <img src="${feedItem["img"]}" width="160" height="90" alt="thumbnail" loading="lazy"/>
  </a>
  <div class="text-contents">
    <h3 class="title">
      <a href="${feedItem["uri"]}">
    ${feedItem["title"]}
      </a>
    </h3>
    <p>
      ${feedItem["snippet"]} ...
    </p>
  </div>
</article>
`;
            form.result.textContent += feedItem["card"];
          }
        });
      });
    </script>
  </body>
</html>

次は使い方の説明です

コードばかりになってしまったので次の記事で使い方を説明したいと思います。

関連記事

Blog Archive

QooQ