はてなブログカードの iframe 内を弄る(自サイトのみ)


外部リンクを別ウィンドウで開く方法

はてなブログでは、公式のエディタを使ってリンクを挿入しますと外部サイトであっても同じウィンドウ(タブ)で開いてしまいます。

HTML編集で target="_blank" を直書きすればいいのですが、面倒ですし、うっかり忘れたりすることも多いので、当サイトでは、javascript で target="_blank" を挿入するようにしています。興味のある方は下記記事をご覧ください。



ブログカードはすべてのリンクが別ウィンドウで開く

で、この記事の本題はそのことではなく、ブログカードでリンクを挿入した場合には逆に自サイトも別ウィンドウで開いてしまうのをなんとかしようというものです。

ブログカードとはこれです。

www.imuza.com


HTMLソースを見ますとこうなっています。

<iframe
  src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.imuza.com%2Fentry%2F2019%2F01%2F07%2F210817"
  title="はてなブログ簡単カスタマイズ imzModules バージョンアップ - IMUZA.com"
  class="embed-card embed-blogcard"
  scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe>
<cite class="hatena-citation">
  <a href="https://www.imuza.com/entry/2019/01/07/210817">www.imuza.com</a>
</cite>


iframe 内のリンクを見てみますと、

<a href="https://www.imuza.com/entry/2019/01/07/210817" target="_blank">はてなブログ簡単カスタマイズ imzModules バージョンアップ</a>

のように、target="_blank" が挿入されています。これ、当たり前で、何も指定しないと iframe内に表示されてしまいます。


で、この target要素の値を自サイトのブログカードだけ _parent に変更できないかということです。


クロスドメインで iframe内を弄れない

iframe内のコンテンツを取得するには contentWindow.document プロパティがあります。ただ、これが使えるのはドメイン名が同じ自サイトに限ります。ドメインが違う場合は次のようなクロスドメインのエラーが出ます。


Uncaught DOMException: Blocked a frame with origin "https://www.imuza.com" from accessing a cross-origin frame.


え、自サイトなのに? と思うのですが、ブログカードのソースを見てみますと、src属性が https://hatenablog-parts.com になっていますのでクロスドメインになってしまいます。独自ドメインといえどもブログシステムのバーチャルドメイン(でいいのかな?)ですので当然です。


自サイト名/embed でもブログカードはできる

いろいろ調べますと、クロスドメイン通信ができる window.postMessage というメソッドがあることがわかったのですが、これで可能かどうかは別にしても、外部サイト側(iframe内のページ)にもスクリプトが必要になるというとても面倒なことになりますので早々にあきらめ、さらに調べましたら、iframeの src属性が 自サイト名/embed/エントリー でもブログカードはできることがわかりました。

たとえば、ブログカードにしたい自サイトのページが https://www.imuza.com/entry/2019/01/07/210817 であれば、iframe の src属性にそのURLの entryembed に差し替えた値を指定すればクロスドメインにならずにブログカードをつくることができます。

手作業で行う場合は、見たまま編集に限りますが、ブログカードを挿入した後にHTML編集で src属性を変更しておけば、後は表示する際に javascriptで aタグの taget属性を変更することが可能になるはずです。


iframe内の aタグを target=_parent に変更する javascript

<script>
window.addEventListener('load', function(){
    var contents = document.getElementById('main'); // メインコンテンツ内のiframeに限定するため
    var cards = contents.getElementsByTagName('iframe');
    Array.prototype.forEach.call(cards,function(card){
        var cFlag = card.classList.contains('embed-blogcard'); // ブログカードか?
        var reg = new RegExp('^http(s)?://' + location.hostname + '.+$'); // 自サイトか?
        if(cFlag && reg.test(card.src)){
            var body = card.contentWindow.document;
            var as = body.getElementsByTagName('a');
            Array.prototype.forEach.call(as, function(a){
                a.target = '_parent';
            });
        }
    });
}, false);
</script>


このスクリプトをフッタにでも入れておけば、iframeの src属性が自サイトに変更されていればブログカードは同じウィンドウで開きます。イベントが発火するタイミングは iframe内のドキュメントが読み込まれていないと意味がありませんので、すべてのリソースが読み込まれた後に発火する loadにします。


iframeの src属性を自サイトに変更する javascript

(2019/1/20)この項目の内容はうまくいかない場合があります。原因は調査中です。


<script>
document.addEventListener('DOMContentLoaded', function(){
    var article = document.getElementsByTagName('article')[0];
    if(article !== undefined){
        var iframes = article.getElementsByTagName('iframe');
        var reg = new RegExp('http(s)?%3A%2F%2F' + location.hostname + '.+$');
        Array.prototype.forEach.call(iframes, function(iframe){
            var embedUrl = reg.exec(iframe.src);
            if(embedUrl !== null) iframe.src = decodeURIComponent(embedUrl[0]).replace('entry', 'embed');
        });
    }
}, false);
</script>


iframeの src属性は iframe内が展開される前に変更しておかないとクロスドメインになってしまうようですので、イベントの発火は COMContentLoadedを使います。


ということで、ふたつ合わせて次のスクリプトをフッタにでも入れておけば、はてな公式のエディタで挿入したブログカードのうち、自サイトのものは同じウィンドウで開くはずです。


完成

(2019/1/20)この項目の内容はうまくいかない場合があります。原因は調査中です。


<script>
document.addEventListener('DOMContentLoaded', function(){
    var article = document.getElementsByTagName('article')[0];
    if(article !== undefined){
        var iframes = article.getElementsByTagName('iframe');
        var reg = new RegExp('http(s)?%3A%2F%2F' + location.hostname + '.+$');
        Array.prototype.forEach.call(iframes, function(iframe){
            var embedUrl = reg.exec(iframe.src);
            if(embedUrl !== null) iframe.src = decodeURIComponent(embedUrl[0]).replace('entry', 'embed');
        });
    }
}, false);

window.addEventListener('load', function(){
    var contents = document.getElementById('main'); // メインコンテンツ内のiframeに限定するため
    var cards = contents.getElementsByTagName('iframe');
    Array.prototype.forEach.call(cards,function(card){
        var cFlag = card.classList.contains('embed-blogcard'); // ブログカードか?
        var reg = new RegExp('^http(s)?://' + location.hostname + '.+$'); // 自サイトか?
        if(cFlag && reg.test(card.src)){
            var body = card.contentWindow.document;
            var as = body.getElementsByTagName('a');
            Array.prototype.forEach.call(as, function(a){
                a.target = '_parent';
            });
        }
    });
}, false);
</script>


何だか面倒なことになりますね。他にいい方法はないものでしょうか?