WordPressで「はてなブログカード」のような埋め込み機能作成

ブログカード

最近あちこちのブログで見かける「はてなブログカード」と言う機能があります

WordPress で利用するには、このあたりの情報が大元のようです

はてなブログの記事を紹介しやすくしました。URLを貼るだけで、コンパクトに整ったブログカードを貼り付けることができます【追記あり】 – はてなブログ開発ブログ
はてなブログでは、さまざまなコンテンツを貼り付けできる「リンク挿入」機能を強化し、はてなブログの記事を簡単に、きれいに整った体裁で貼り付けることができるようにしました。はてなブログで運営されているブログを紹介したり記事に言及する際に、この 記事貼り付け 機能を利用すると、記事タイトルや、本文の概要、アイキャッチ画像などがコンパクトにまとまった ブログカード 形式で、次のように貼り付けることができます。あわせて、自分がこれまでに投稿した記事を簡単に挿入できるように、そのブログの「過去記事貼り付け」機能を、編集サイドバーに追加しました。 ※「記事貼り付け」できるのは、はてなブログの記事だけです。 …
はてなブログの記事を紹介しやすくしました。URLを貼るだけで、コンパクトに整ったブログカードを貼り付けることができます【追記あり】 - はてなブログ開発ブログ
はてなブログのブログカードがOGPに対応! はてなブログ以外もブログカードスタイルになるぞ
はてなブログのブログカードが拡張され、OGPにも対応しました。これによりOGPが設定されているサイトを記事中にリンクするとき、ブログカード形式で挿入することができるようになります。
はてなブログのブログカードがOGPに対応! はてなブログ以外もブログカードスタイルになるぞ

ここまで見て、あれ ブログカードがちょっと違うぞと気がついたと思いますが、これが今回作成したブログカードです。詳しくは後ほど紹介します。

はてなのブログカードは OGPにも対応しているようで、はてなブログ以外のサイトもブログカード形式で表示出来るようです

ちなみにOGPは、ツイッターやフェースブックで利用されている機能で記事の要約だけでなく画像等もメタデータとして含むことが出来ます

Open Graph protocol
The Open Graph protocol enables any web page to become a rich object in a social graph.
Open Graph protocol

OGPデータからカード作成するなら、プログラム組めば以外に簡単に出来るんじゃない?

と思ったのが今回のきっかけです (^^)

ということでちょっと調べると はてなのブログカードを iframe を使ってブックマークレットから簡単に生成する方法を公開してくれている方がいます

それを利用してブックマークレットからそのサイトのカードを作ってみます

最初はこの iframe をプログラムで処理しようとも思ったのですが、はてな経由のブログカードだと参照元やピンバックの問題があるようなので、単純にOGPを取得してリンクを はてなブログカードのように表示させてみました

OGPブログカード

ブログカード作成でどこを はてなブログカード に似せて、どこを変えるかがポイントです

  • カードサイズ ー 少し大きめの最大幅 600px
  • サムネイル画像 ー WordPress サムネイルのデフォルト 150px 想定
  • 画像のキャッシュ ー サムネイルとアイコンは取得してキャッシュ化する
  • はてブ数 ー 不要

レスポンシブ対応とするので、サムネイル画像がうまい具合に縮小するのはトリミングした場合なのですが、そこは選択できるように WordPress の設定に従っています

使い方は簡単で記事に URL を貼り付けるだけです

youtube 等を埋め込む場合と同じ要領です

ダウンロード

このプログラムを使いたい方、コードに興味がある方は、WordPress Plugin : Celtispack ページからダウンロードすることが出来ます

 

以下プログラム作成時のポイント等について少し紹介します

埋め込み oEmbed として作成するか?
wp_oembed_add_provider('http://*', 'http://hatenablog.com/oembed');

はてなのように独自のエンドポイントを決めてあたかも oEmbed 機能が動作してるがごとく実装しようと思ったのですが、エンドポイントのプロバイダーを選択するのに http://* というのはよろしくないです

例えば、上記の hatena のエンドポイントの登録では、記事中に埋め込まれた URL のどのようなパターンにもマッチするということなので、本当は他のエンドポイントを使いたいのに はてな が使われる可能性があるということになってしまうので wp_oembed_add_provider で登録する場合は ‘http://.+hate.+’ のように対象をしぼる必要があります

ということでプログラムとしては、エンドポイントを登録はせずに、該当するエンドポイントが見つからない場合の処理としてフィルターフックを使って実装しました

コードを調べると WP4.0 から追加された oembed_ttl というフックポイントが使えそうだったのでそこにフックして機能するようにしています

従って、このOGPブログカードが使えるのは WP4.0 以上となります

※はてなブログカードと併用したい場合は、はてなのエンドポイントの登録を 'http://*' から ‘http://.+hate.+’ にすれば独自ドメイン以外とは併用できます。また、Hatena ブログカードを直接 HTML に iframe で埋め込んでも併用できます

プログラム

oembed_ttl にフックする処理です

    /**
     * Filter the oEmbed TTL value (time to live).
     *
     * @since 4.0.0
     *
     * @param int    $time    Time to live (in seconds).
     * @param string $url     The attempted embed URL.
     * @param array  $attr    An array of shortcode attributes.
     * @param int    $post_ID Post ID.
     */
    // oEmbed 埋め込みURL で provider がマッチしない時に OGPブログカード html データを生成する
    // また、記事更新時はキャッシュは使わないで強制的に記事内の全ての oEmbed も更新する
    function oembed_ogp( $time, $url, $attr, $post_ID ) {
        
        $option = $this->option;
	if ( function_exists( '_wp_oembed_get_object' ) ){
            $key_suffix = md5( $url . serialize( $attr ) );
            $cachekey = '_oembed_' . $key_suffix;
            $cachekey_time = '_oembed_time_' . $key_suffix;

	    $cache = get_post_meta( $post_ID, $cachekey, true );

       	    $attr['discover'] = false;
            $provider = '';
            $html = '';
            if ( !empty($option['oembed-blogcard'])){
                $oembed = _wp_oembed_get_object();
                $provider = $oembed->get_provider( $url, $attr );
            }
            if ( empty($GLOBALS['wp_embed']->usecache) ) { 
                $html = wp_oembed_get( $url, $attr );
                if (empty($html) && empty($provider) && !empty($option['oembed-blogcard'])){
                    $html = $this->ogpcard_html_get($url);
                }
            }
            elseif (empty($cache)){
                if(empty($provider) && !empty($option['oembed-blogcard'])){
                    $html = $this->ogpcard_html_get($url);
                }
            }
            //embed html 更新
            if ( !empty($html) ) {  
                update_post_meta( $post_ID, $cachekey, $html );
                update_post_meta( $post_ID, $cachekey_time, time() );
                $cache = get_post_meta( $post_ID, $cachekey, true );
	    } 
        }
        return($time);
    }

oEmbed 処理は、通常埋め込む HTML を対象サイトから取得してデータベースにキャッシュとして保存しています

毎回、データを通信で取得するのはなかなかコストのかかる処理なのでキャッシュをうまく使わないと重くてどうにもならなくなってしまいますので注意です

次にOGPを使ってデータを取得する処理のメイン部分です

    //OGPデータからブログカード用HTML生成
    public function ogpcard_html_get( $url ) {

        $html = '';
        $args = array( 'timeout' => 10, 'httpversion' => '1.1' );
        $response = wp_safe_remote_get( $url, $args );
        //サイトによってはタイムアウトとなる場合あるのでデフォルト 5->10秒にして1回だけリトライ
        if ( is_wp_error( $response ) || $response['response']['code'] !== 200 ) {
            $response = wp_safe_remote_get( $url, $args );
        }
        if ( ! is_wp_error( $response ) && $response['response']['code'] === 200 ) {
            $head = Celtis_lib::htmltagsplit($response['body'], '<head', '/head>');
            if(!empty($head[1])){
                //OGP パース
                $ogp = ogp\Parser::parse( mb_convert_encoding($head[1], 'HTML-ENTITIES', 'UTF-8'));                    
                $myurl = get_bloginfo('url');
                //OGP には含まれていないが favicon を head から取得 
                $ogp['og:favicon'] = '';
                preg_match('#<link.+icon.+href=.(.+\.ico|.+\.png)#', $head[1],$match);
                if(!empty($match[1])){
                    $response = wp_safe_remote_get( $match[1] );
                    if ( ! is_wp_error( $response ) && $response['response']['code'] === 200 ) {
                        $type = $response['headers']['content-type'];
                        //アイコンはローカルにキャッシュする
                        $upload = wp_upload_dir();
                        $keyid  = md5( 'icon_' . $match[1]);
                        //Hatena blog の icon は拡張子を ico にしないと IE で表示されない
                        $newext = ($type == 'image/x-icon' || $type == 'image/vnd.microsoft.icon')? 'ico' : 'png';
                        $fname  = $upload['basedir'] . '/celtispack/icon/' . "{$keyid}.{$newext}";
                        $ifp = @ fopen( $fname, 'wb' );
                        if ( $ifp ){
                            @fwrite( $ifp, $response['body'] );
                            fclose( $ifp );
                            clearstatcache();
                            // Set correct file permissions
                            $stat = @ stat( dirname( $fname ) );
                            $perms = $stat['mode'] & 0007777;
                            $perms = $perms & 0000666;
                            @ chmod( $fname, $perms );
                            clearstatcache();
                            $imageurl =  $upload['baseurl'] . '/celtispack/icon/' . "{$keyid}.{$newext}";
                            $ogp['og:favicon'] = '<img width="16" height="16" src="' . $imageurl . '"  class="favicon" />';
                        }
                    }
                }
                $thumbnail = '';
                if(!empty($ogp['og:image'])){
                    $img = (!is_array($ogp['og:image']))? $ogp['og:image'] : $ogp['og:image'][0];
                    $imgsize = array('width' => 150, 'height' => 150);
                    if(!empty($ogp['og:url']) && !preg_match("#$myurl#", $ogp['og:url'])){
                        //自サイト以外の画像はサムネイルを生成してキャッシュ
                        if(class_exists('Celtispack_thumbnail', FALSE)){
                            $module = Celtispack_thumbnail::thumbnail_module_instance();
                            $value = $module->getmake_thumbnail_size( $img, $imgsize, 'thumbnail');
                            if (false !== $value) 
                                $img = $value['url'];
                        }
                    }
                    $default_attr = array(
                        'src'	=> $img,
                        'class'	=> "ogp-thumb",
                        'alt'	=> '',
                    );
                    $imgattr = wp_parse_args($imgsize, $default_attr);
                    $imgattr = array_map( 'esc_attr', $imgattr );
                    $thumbnail = rtrim("<img ");
                    foreach ( $imgattr as $name => $value ) {
                        $thumbnail .= " $name=" . '"' . $value . '"';
                    }
                    $thumbnail .= ' />';
                }
                if(!empty($ogp['og:title']) && !empty($ogp['og:url'])){
                    $og_title = (!is_array($ogp['og:title']))? $ogp['og:title'] : $ogp['og:title'][0];
                    $og_url = (!is_array($ogp['og:url']))? $ogp['og:url'] : $ogp['og:url'][0];
                    $grid = (empty($thumbnail)) ? 'no-thumb' : 'with-thumb';

                    $html .= '<div class="card-wrapper"><div class="card-wrapper-inner">';
                    //カードコンテント
                    $html .=  '<div class="card-content ' .$grid .'">';
                    $html .=    '<h2 class="card-title"><a href="' . $og_url . '" target="_blank">' . $og_title . '</a></h2>';
                    if(!empty($ogp['og:description'])){
                        $og_dsc = (!is_array($ogp['og:description']))? $ogp['og:description'] : $ogp['og:description'][0];
                        $html .='<div class="card-description">' . $og_dsc . '</div>';
                    }
                    $html .=  '</div>';
                    if(!empty($thumbnail)){
                        $html .= '<div class="thumb-wrapper"><a href="' . $og_url . '" target="_blank">' . $thumbnail. '</a></div>';
                    }
                    //カードフッター
                    if(!empty($ogp['og:site_name'])){
                        $og_site = (!is_array($ogp['og:site_name']))? $ogp['og:site_name'] : $ogp['og:site_name'][0];
                        $html .= '<div class="card-footer"><a href="' . $og_url . '" target="_blank">' . $ogp['og:favicon'] . ' ' . $og_site . '</a></div>';
                    }
                    $html .= '</div></div>';
                    //html カスタマイズ用のフィルターフック
                    $html = apply_filters( 'oembed_ogp_dataparse', $html, $ogp, $url );
                }
                if(empty($html)){
                    //head からタイトルを取得してリンクを生成
                    preg_match('#<title>(.+)</title>#', $head[1],$match);
                    $title = (!empty($match[1]))? esc_html($match[1]) : esc_html($url);
                    $html = '<span class="embed-link"><a href="' . esc_url($url) . '">' . $title . '</a></span>';
                }
            }
        }
        return $html;
    }

OGPが複数設定されている場合には、最初のデータを使用しています

OGPデータがない場合は title タグのデータを使って通常のリンクを作成します

カスタマイズを行いたい場合は、oembed_ogp_dataparse というフックポイントを設けていますので、はてブ数を取得するコード等を盛り込むことなどに使えると思います

※OGPデータのパース処理は GitHub で公開されていたコードを使わせて頂いてます
コンパクトでとても扱いやすいライブラリです

PHP Open Graph Library (mapkyca/php-ogp)

ブログカードの CSS

CSSは私の作成している Celtis-one というテーマに合わせてありますが、他のテーマでも概ね問題なく表示できる思います

CSSは苦手なのですが、はてなブログカードぽく仕上がった気がします (^^)

CSSコード

/*
   oEmbed OGP ブログカードのスタイル
  カードサイズは一回り大きいが Hatenaブログカードに近いスタイルにする
*/

.card-wrapper:after,
.card-wrapper:before {
    content: ' ';
    display: table;
}

.card-wrapper:after {
    zoom: 1;
    clear: both;
}

.card-wrapper {
    margin: 0px 0px 1.7rem;
    overflow: hidden;
    width:100%;
    max-width:600px;
    max-height:202px;
    border: 1px solid;
    border-color: #eaeaea #dddddd #d0d0d0;
    border-radius: 5px;
    background-color: #fff;
    background-clip: padding-box;
}

.card-wrapper a,
.card-wrapper a:visited {
    color: #369ecf;
    border: none !important;
    text-decoration: none;
}
.card-wrapper a:focus {
    outline: thin dotted;
}
.card-wrapper a:hover {
    text-decoration: underline;
}

.card-wrapper-inner {
    padding: 12px;
}
.card-wrapper * {
    word-wrap: break-word;
}

.card-content {
	float: left;
	padding-top: 0;
	display: inline;
    max-height: 150px;
    overflow: hidden;
}

.card-wrapper .no-thumb {
	width: 100%;
	margin: 0 0 12px 0 !important;
}

.card-wrapper .with-thumb {
	width: 72%;
	margin-right: 1.8%;
    margin-bottom: 12px;    
}

.card-wrapper .thumb-wrapper {
	float: left;
	padding-top: 0;
	width: 26.2%;
    max-height: 150px;
	margin: 0 0 12px 0 !important;
}

.card-content .card-title {
    font-size: 16px;
    line-height: 1.5;
    max-height: 48px;
    overflow: hidden;
    margin: 0 0 3px;
    padding: 0;
    border: none;   
    background: none;
}
.card-content .card-title a {
    color: #333333;
}
.card-content .card-description {
    font-size: 12px;
    line-height: 1.4;
    max-height: 50px;
    overflow: hidden;
}

.card-footer {
    zoom: 1;
    clear: both;
    margin-top: 8px;
    padding-top: 5px;
    border-top: 1px solid #eaeaea;
    height: 15px;
    position: relative;
    font-size: 11px;
}
.card-footer img {
    display: inline !important;
}
.card-footer a,
.card-footer a:visited,
.card-footer a:hover,
.card-footer a:focus,
.card-footer a:active {
    color: #999999;
}

表示が崩れる場合は CSSで調整してみてください m(__)m

ちなみに記事作成時のビジュアルモードでもカード表示されるように TinyMCE エディターにもCSSをロードしていますので、こんな感じで表示されます

BlogCard-embed

以上

WordPress 4.0 以上で使用できる「はてなブログカード」のような埋め込み機能のプログラムを紹介しました

まだ不具合等があるかもしれませんが、よろしければ試してみてください (^^)


まとめ記事紹介

go-to-top