×

前回ご紹介したプラグイン。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

関連投稿

YP-D7 (5)アームを磨きまくって美麗に

入手した2本目のトーンアーム。かなり煤けて黒ずんでいますから、こちらを磨いていきます。ただ酸化腐食は...

タグ:

メヒシバ退治 (2)アージラン投入

メヒシバ撲滅、 天候も回復したので、アージランを投下していきたいと思います。  まず、メヒシバ地帯を...