×

前回ご紹介したプラグイン。URLを入力すると美麗なバナーを制作してくれるというものでした。

今回はこれを機能拡張します。具体的にいうと、
・ 画像URLを入力するとそちらの画像パスを優先し、
・ 画像URLが空値なら、OGP画像を探しに行く

というものです。
OGP画像を準備していないサイト・ページがあったり、あるいは巧く取得できなかった場合に画像の絶対パスを指定することで必ずリカバリーができる、というものです。
実例をお見せしましょう。

今回用意したVer2の実装はこんな感じ。
記事のURLとは別に、画像のURLが入力できるようになっています。

そして、画像URLが入力されていない場合はOGP画像を探しに行くし、そうでなければURLどおりの画像を表示する、というバナーです。
そして、画像URLを入力した結果がこちら:

「コリャやべーわw」 地図会社公式が投稿の”想像超斜め上な標識”にSNS話題沸騰…何が? 「言われたらそうだけど」(1/2 ページ) | 乗りものニュース
地図会社のゼンリンの公式SNSアカウントが、とある標識を写真で紹介しました。この標識は「大仏町」と描かれていますが、これが大きな話題を呼び、Xのインプレッション数が100万を超えています。どういったものだったのでしょうか。(1/2 ページ)
trafficnews.jp

前回のコードより安定度も増し、一層盤石なプラグインとなりました。
画像の指定があればそれを優先、なければOGP画像を探す、それでもなければ画像なしのバナーとなります。

サンプルコードを以下に示します。どんなWordPressデザインでもそれなりに動くと思いますのでご活用されてください。

設置パスは前回同様:

–フォルダパス–
wordpress
 ┗wp-content
┗plugins
┗url-banner-block
┣ ★block.js
┗ ★url-banner-block.php

block.js

(function (blocks, element, components, apiFetch) {

    var el = element.createElement;
    var TextControl = components.TextControl;

    blocks.registerBlockType('myplugin/url-banner', {
        title: 'URLリンクバナー',
        icon: 'smiley',
        category: 'widgets',

        attributes: {
            url: { type: 'string', default: '' },
            imageUrl: { type: 'string', default: '' },
            preview: { type: 'object', default: null }
        },

        edit: function (props) {

            function fetchPreview(value) {

                if (!value) return;

                apiFetch({
                    path: '/url-banner/v1/preview?url=' + encodeURIComponent(value)
                }).then(function (data) {

                    props.setAttributes({
                        preview: data
                    });

                }).catch(function () {

                    props.setAttributes({
                        preview: null
                    });

                });
            }

            function getHostname(url) {
                try {
                    return new URL(url).hostname;
                } catch (e) {
                    return '';
                }
            }

            var preview = props.attributes.preview;

            var imageSrc =
                props.attributes.imageUrl ||
                (preview && preview.image);

            return el('div', {},

                // URL入力
                el(TextControl, {
                    label: '記事URL',
                    value: props.attributes.url,
                    onChange: function (value) {

                        props.setAttributes({ url: value });

                        if (value) {
                            fetchPreview(value);
                        }

                    }
                }),

                // 画像URL入力
                el(TextControl, {
                    label: '画像URL(任意・OGPより優先)',
                    value: props.attributes.imageUrl,
                    onChange: function (value) {

                        props.setAttributes({
                            imageUrl: value
                        });

                    }
                }),

                preview &&

                el('a',
                    {
                        href: props.attributes.url,
                        target: '_blank',
                        rel: 'noopener noreferrer',
                        style: {
                            display: 'flex',
                            border: '1px solid #ddd',
                            borderRadius: '8px',
                            overflow: 'hidden',
                            textDecoration: 'none',
                            color: '#000',
                            marginTop: '12px',
                            backgroundColor: '#fff'
                        }
                    },

                    // 画像
                    imageSrc &&
                    el('div',
                        {
                            style: {
                                flex: '0 0 120px',
                                backgroundColor: '#f5f5f5'
                            }
                        },
                        el('img', {
                            src: imageSrc,
                            style: {
                                width: '120px',
                                height: '100%',
                                objectFit: 'cover',
                                display: 'block'
                            }
                        })
                    ),

                    // テキスト
                    el('div',
                        {
                            style: {
                                padding: '12px',
                                flex: '1'
                            }
                        },

                        el('div',
                            {
                                style: {
                                    fontWeight: 'bold',
                                    marginBottom: '6px',
                                    fontSize: '14px',
                                    lineHeight: '1.4'
                                }
                            },
                            preview.title
                        ),

                        preview.description &&
                        el('div',
                            {
                                style: {
                                    fontSize: '12px',
                                    color: '#555',
                                    lineHeight: '1.4'
                                }
                            },
                            preview.description
                        ),

                        el('div',
                            {
                                style: {
                                    fontSize: '11px',
                                    color: '#888',
                                    marginTop: '6px'
                                }
                            },
                            getHostname(props.attributes.url)
                        )

                    )
                )

            );
        },

        save: function () {
            return null;
        }

    });

})(
    window.wp.blocks,
    window.wp.element,
    window.wp.components,
    window.wp.apiFetch
);

url-banner-block.php

<?php
/*
Plugin Name: URL Banner Block
*/

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/*
|--------------------------------------------------------------------------
| ブロック登録
|--------------------------------------------------------------------------
*/
function url_banner_block_init() {

    wp_register_script(
        'url-banner-block',
        plugins_url( 'block.js', __FILE__ ),
        array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-api-fetch' ),
        filemtime( plugin_dir_path( __FILE__ ) . 'block.js' )
    );

    register_block_type( 'myplugin/url-banner', array(
        'editor_script'   => 'url-banner-block',
        'render_callback' => 'url_banner_render',
    ) );
}
add_action( 'init', 'url_banner_block_init' );


/*
|--------------------------------------------------------------------------
| REST API(プレビュー取得)
|--------------------------------------------------------------------------
*/
add_action( 'rest_api_init', function () {
    register_rest_route( 'url-banner/v1', '/preview', array(
        'methods'  => 'GET',
        'callback' => 'url_banner_preview_callback',
        'permission_callback' => '__return_true',
    ) );
});


function url_banner_preview_callback( WP_REST_Request $request ) {

    $url = esc_url_raw( $request->get_param( 'url' ) );

    if ( empty( $url ) ) {
        return array(
            'title' => '',
            'description' => '',
            'image' => ''
        );
    }

    $response = wp_remote_get( $url, array(
        'timeout'     => 15,
        'redirection' => 5,
        'user-agent'  => 'WordPress URL Banner Preview'
    ) );

    if ( is_wp_error( $response ) ) {
        return array(
            'title' => 'Error',
            'description' => $response->get_error_message(),
            'image' => ''
        );
    }

    $body = wp_remote_retrieve_body( $response );

    if ( empty( $body ) ) {
        return array(
            'title' => '',
            'description' => '',
            'image' => ''
        );
    }

    libxml_use_internal_errors( true );

    $dom = new DOMDocument();
    $body = mb_convert_encoding( $body, 'HTML-ENTITIES', 'UTF-8' );
    $dom->loadHTML( $body );

    $xpath = new DOMXPath( $dom );

    $title = '';
    $description = '';
    $image = '';

    $meta_nodes = $xpath->query('//meta');

    foreach ( $meta_nodes as $meta ) {

        if ( $meta->hasAttribute('property') ) {

            $property = strtolower( $meta->getAttribute('property') );
            $content  = $meta->getAttribute('content');

            if ( $property === 'og:title' && empty( $title ) ) {
                $title = $content;
            }

            if ( $property === 'og:description' && empty( $description ) ) {
                $description = $content;
            }

            if ( $property === 'og:image' && empty( $image ) ) {
                $image = $content;
            }
        }

        if ( $meta->hasAttribute('name') ) {

            $name    = strtolower( $meta->getAttribute('name') );
            $content = $meta->getAttribute('content');

            if ( $name === 'description' && empty( $description ) ) {
                $description = $content;
            }
        }
    }

    if ( empty( $title ) ) {
        $title_nodes = $xpath->query('//title');
        if ( $title_nodes->length > 0 ) {
            $title = $title_nodes->item(0)->nodeValue;
        }
    }

    return array(
        'title'       => wp_strip_all_tags( $title ),
        'description' => wp_strip_all_tags( $description ),
        'image'       => esc_url_raw( $image ),
    );
}


/*
|--------------------------------------------------------------------------
| フロント表示(動的レンダリング)
|--------------------------------------------------------------------------
*/
function url_banner_render( $attributes ) {

    if ( empty( $attributes['url'] ) ) {
        return '';
    }

    $url = esc_url_raw( $attributes['url'] );

    $cache_key = 'url_banner_' . md5( $url . ( $attributes['imageUrl'] ?? '' ) );
    $cached = get_transient( $cache_key );

    if ( $cached !== false ) {
        return $cached;
    }

    $request = new WP_REST_Request( 'GET' );
    $request->set_param( 'url', $url );

    $data = url_banner_preview_callback( $request );
    
    if ( ! empty( $attributes['imageUrl'] ) ) {
        $data['image'] = esc_url_raw( $attributes['imageUrl'] );
    }

    if ( empty( $data['title'] ) ) {
        return '';
    }

    ob_start();
    ?>

    <a href="<?php echo esc_url( $url ); ?>" target="_blank" rel="noopener noreferrer"
       style="display:flex;border:1px solid #ddd;border-radius:8px;overflow:hidden;text-decoration:none;color:#000;background:#fff;margin:30px 0;">

        <?php if ( ! empty( $data['image'] ) ) : ?>
            <div style="flex:0 0 120px;background:#f5f5f5;">
                <img src="<?php echo esc_url( $data['image'] ); ?>"
                     style="width:120px;height:100%;object-fit:cover;display:block;">
            </div>
        <?php endif; ?>

        <div style="padding:12px;flex:1;">
            <div style="font-weight:bold;margin-bottom:6px;font-size:16px;line-height:1.4;">
                <?php echo esc_html( $data['title'] ); ?>
            </div>

            <?php if ( ! empty( $data['description'] ) ) : ?>
                <div style="font-size:13px;color:#555;line-height:1.4;">
                    <?php echo esc_html( $data['description'] ); ?>
                </div>
            <?php endif; ?>

            <div style="font-size:11px;color:#888;margin-top:6px;">
                <?php echo esc_html( parse_url( $url, PHP_URL_HOST ) ); ?>
            </div>
        </div>

    </a>

    <?php

    $html = ob_get_clean();

    set_transient( $cache_key, $html, 12 * HOUR_IN_SECONDS );

    return $html;
}

シリコンパワー ノートPC用メモリ DDR4-2400(PC4-19200) 8GB×1枚 260Pin 1.2V CL17 SP008GBSFU240B02

Synology NASを拡張した時に入れたメモリーがコレ!永久保証の上、レビューも高評価。もちろん正常に動作しており、速度余裕も生まれて快適です。

フィリップス 電動歯ブラシ ソニッケアー 3100シリーズ (軽量) HX3673/33 ホワイト 【Amazon.co.jp限定・2024年モデル】

歯の健康を考えるのならPhilipsの電動歯ブラシがお勧めです。歯科医の推奨も多いみたいです。高価なモデルも良いですが、最安価なモデルでも十分に良さを体感できる。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

投稿者

KeroYon

関連投稿

LINE Clovaの記事をサマライズした

ようやく LINE Clova WAVE の制作記をひとつにまとめました。 これでLoudspeak...

WordPress上のリンクバナー、軽やかにDIY実現

表題のとおりです。 私は長年(といっても5年ほど)アメブロのお世話になっていて、そこではURLを入力...

進化しすぎたオートは熟練者を切り捨てる 〜Mac新世代IME

新しいMac OSでの日本語入力。この非常に素晴らしい、まさに「新世代の」日本語入力。それの美点と欠...

オーディオ関連の4書籍をご紹介

自炊しようと、本棚の古本を漁っていたら・・・ アラ、こんなモノも持っていたのか?・・・という珍書がざ...

ScanSnap 見開きページの再現要件

見開き表示実現の意義 自炊において、見開きページを見開き表示のままで完全再現することにこだわっている...

新しいiPad

新しいiPadを買いました。 新しい・・・とは言っても、新品ではありません。中古です。これまで使って...