はてなブログカードを DIV要素で自作する(自サイトのみ)

相変わらずブログカードにこだわっています。 といってもこのところなかなか時間が取れず、10日ぶりくらいにこだわってみました(笑)。

何をやろうとしているかといいますと、自サイトの記事をブログカードにした場合に別ウィンドウ(タブ)ではなく、つまり target=_blank ではなく、target=_top にしたいということです。


で、前回で oEmbed の仕様もわかり、heroku でサーバも立ち上げて成功はしているのですが、結局のところ、はてなブログの記事にはきっちりと OGPデータが付与されているわけですから、わざわざサーバなどという大層なことをしなくても、HTMLファイルを読み込んで OGPデータからブログカードを作ればいいんじゃないか、そうすれば iframeを使わなくても div要素で行けるはずです。



XMLHttpRequest(XHR)

読み込まれたファイルからさらにウェブサーバと通信するには XMLHttpRequestという組み込みオブジェクトを利用するようです。


developer.mozilla.org


とりあえず、XMLHttpRequest.onreadystatechange - Web API | MDN のサンプルコードを利用して htmlファイルが読み込めるかやってみましょう。

var xhr = new XMLHttpRequest(),
    method = "GET",
    url = "http://ausnichts.hateblo.jp/entry/imzmodules"; // 自サイトのページ

xhr.open(method, url, true);
xhr.onreadystatechange = function () {
  if(xhr.readyState === 4 && xhr.status === 200) {
      console.log(xhr.responseText);
  }
};
xhr.send();


f:id:ausnichts:20190310140157j:plain

確かに読み込めています。ここから OGPデータを取り出せばいいのですが、このままの単なる文字列では使いづらいですので、DOMParserを使って DOMオブジェクトにパースしましょう。


DOMParser

developer.mozilla.org


「実験的な機能」とあります。とりあえずやってみます。


var xhr = new XMLHttpRequest(),
    method = "GET",
    url = "http://ausnichts.hateblo.jp/entry/imzmodules"; // 自サイトのページ
var parser = new DOMParser();

xhr.open(method, url, true);
xhr.onreadystatechange = function () {
  if(xhr.readyState === 4 && xhr.status === 200) {
      var doc = parser.parseFromString(xhr.responseText, "text/html");
      console.log(doc);
  }
};
xhr.send();


f:id:ausnichts:20190310140428j:plain

ちゃんと DOMオブジェクトになっています。あとは OGPデータを取り出してそれぞれ DIV要素の中に入れ込めばOKということになります。


OGPデータを取り出してブログカードにする

では、実際に記事内にブログカードを作ってみましょう。方法は、ブログカードを置きたい場所にその記事の URLを置いておき、それを Javascriptでブログカードのコードに差し替えることでいけると思います。


記事内の HTMLをこんな感じで置いておきます。

<div id="blogcard">http://ausnichts.hateblo.jp/entry/imzmodules</div>


フッタに Javascriptを入れます。

<script>
(function(){
var url = 'http://ausnichts.hateblo.jp/entry/imzmodules';
var xhr = new XMLHttpRequest();
var parser = new DOMParser();

xhr.open('GET', url);
xhr.send();
 
xhr.onreadystatechange = function() {
    if(xhr.readyState === 4 && xhr.status === 200) {
      
        var doc = parser.parseFromString(xhr.response, "text/html");
        var meta = doc.getElementsByTagName('meta');
        var og = {};
        var re = /og:(.+)/;
        for(var i=0; i<meta.length; i++){
            var p = meta[i].getAttribute('property');
            if(p !== null){
                var matches = p.match(re);
                if(matches !== null){
                    og[matches[1]] = meta[i].getAttribute('content');
                }
            }
        }
        var re = /(.+) - .+/;
        og.title = og.title.match(re)[1];
        og.site_url = og.url.split('/')[2];
        og.description = og.description.substr(0, 100) + '...';

        var blogcard = '<div class="thumb-wrapper">'
            + '<a href="' + og.url + '" target= "_top" class="thumb-link">'
            + '<img src="' + og.image + '" class="thumb"></a></div>'
            + '<p class="title">'
            + '<a href="' + og.url + '" target= "_top" class="title-link">' + og.title + '</a></p>'
            + '<p class="description">' + og.description + '</p>'
            + '<div class="footer"><p class="fav-wrap">'
            + '<a href="' + og.site_url + '" target="_top">'
            + '<img src="http://favicon.hatena.ne.jp/?url=' + og.url + '" class="favicon" />' + og.site_name + '</a>'
            + '<img src="http://b.hatena.ne.jp/entry/image/' + og.url + '" class="hatebu"></p></div>';
        document.getElementById('blogcard').innerHTML = blogcard;
    }
}
})();
</script>


f:id:ausnichts:20190310143518j:plain

上がオリジナルのブログカードで、下がはてなのブログカードです。下のリンクは別タブで開きますが、上はそのタブで開きます。


自サイト用オリジナルブログカード

実装するには複数のブログカードに対応しないといけませんので最終的に次のようになりました。

マークアップ

<div class="imzBlogcard">http://ausnichts.hateblo.jp/entry/imzmodules</div>


Javascript

<script>
(function(){
    var cards = document.getElementsByClassName('imzBlogcard');   
    Array.prototype.forEach.call(cards, function(card){
        var url = card.children[0].innerHTML;
        var xhr = new XMLHttpRequest();
        var parser = new DOMParser();

        xhr.open('GET', url);
        xhr.send();
 
        xhr.onreadystatechange = function() {
            if(xhr.readyState === 4 && xhr.status === 200) {
      
                var doc = parser.parseFromString(xhr.response, "text/html");
                var meta = doc.getElementsByTagName('meta');
                var og = {};
                var re = /og:(.+)/;
                for(var i=0; i<meta.length; i++){
                    var p = meta[i].getAttribute('property');
                    if(p !== null){
                        var matches = p.match(re);
                        if(matches !== null){
                            og[matches[1]] = meta[i].getAttribute('content');
                        }
                    }
                }
                var re = /(.+) - .+/;
                og.title = og.title.match(re)[1];
                og.site_url = og.url.split('/')[2];
                og.description = og.description.substr(0, 100) + '...';

                var blogcard = '<div class="thumb-wrapper">'
                    + '<a href="' + og.url + '" target= "_top" class="thumb-link">'
                    + '<img src="' + og.image + '" class="thumb"></a></div>'
                    + '<p class="title">'
                    + '<a href="' + og.url + '" target= "_top" class="title-link">' + og.title + '</a></p>'
                    + '<p class="description">' + og.description + '</p>'
                    + '<div class="footer"><p class="fav-wrap">'
                    + '<a href="' + og.site_url + '" target="_top">'
                    + '<img src="http://favicon.hatena.ne.jp/?url=' + og.url + '" class="favicon" />' + og.site_name + '</a>'
                    + '<img src="http://b.hatena.ne.jp/entry/image/' + og.url + '" class="hatebu"></p></div>';
                card.innerHTML = blogcard;
            }
        }
    });
})();
</script>


サンプルCSS

.imzBlogcard a {
    text-decoration: none;
}
.imzBlogcard a:hover {
    text-decoration: underline;
}
.imzBlogcard {
    width: 100%;
    max-width: 500px;
    background: #fff;
    border: 1px solid #ccc;
    border-radius: 3px;
    box-sizing: border-box;
    padding: 12px;
}
.imzBlogcard .thumb-wrapper{
    width: 100px;
    height: 100px;
    float: right;
    margin: 0 0 10px 10px;
    padding: 0;
    position: relative;
    overflow: hidden;
}
.imzBlogcard .thumb-link {
    position: absolute;
    width: 1000%;
    left: 50%;
    margin: 0 0 0 -500%;
    text-align: center;
}
.imzBlogcard .thumb {
    width: auto;
    height: 100px;
}
.imzBlogcard .title {
    line-height: 1.3;
}
.imzBlogcard .title-link {
    color: #333;
    font-weight: bold;
    font-size: 17px;
    line-height: 1.4;
}

.imzBlogcard .description {
    color: #666;
    font-size: 12px;
    line-height: 1.5;
}
.imzBlogcard .footer {
    clear: both;
}
.imzBlogcard .fav-wrap {
    color: #999;
    margin: 5px 0 0 0;
    font-size: 12px;
}
.imzBlogcard .hatebu {
    margin: 0 0 0 5px;
    border: none;
    display: inline;
    vertical-align: middle;
}

完成です。Javascriptはもう少し整理できそうには思います。

ブラウザの対応ですが、Chrome, Firefox, iPhone/Safari, iPhone/Chrome, Android/Chrome の最新版は問題ありません。Edge, IE は(今日ところは)未確認です。