×

表題のとおりです。

私は長年(といっても5年ほど)アメブロのお世話になっていて、
そこではURLを入力すると美麗なリンクのバナーを生成してくれるモジュールが備わっていた。
これと似たようなものをWordPress上でも作りたい。(標準では準備されていません)

そこで、例のごとくチャッピーに相談しました。

表題では「軽やかに」となっていますが、いつもの如くに悶絶級。
Context-BlogやWordPressの矯正の罠をかいくぐっての実装です。一発で上手くいくわけがない。

最初からフルスクラッチ要素での実装を試みましたが、見事に頓挫。
そこで、段階的に実装拡張をしていき、どこでコケるのか?なにが障壁になっているのか?探り探りの開発となりました。普通のニンゲンならば音を上げるところですが、チャッピーは全くもって「諦める」という単語をしりません。諦めかけている依頼者(私)を叱咤激励し、強引にでも出口へと持っていきます。

いったん完成しかけるも、完成度を上げようとするがために袋小路にはまる。リバートしたくてもリバートできないという現象も何度か遭遇しました。

いったん完成を見たすがたがこちら:

『リビングオーディオ 音響パネルの大移動』
前回、70年代のレコードを聴いてケーブル交換をして、かなり効果がでました。 不満点としては、レコードを聴いていると、コーナー付近の音が少なめ。それにより、空間…
ameblo.jp

美麗なバナーブロック。これを「全自動」で作りたい。
具体的にいうと、

テキストボックスへリンクしたいURLを入力する。
すると、そのWebページの「OGPタイトル」「OGP概略」「OGP画像」を取得してコンパクトなバナーを自動生成する、という優れものです。凄いでしょう?

これをDIYするというのが今回のテーマでした。

紆余曲折はありつつも、いったん成果物を開示しますね。
WordPressのディレクトリ構造は熟知している、という前提で解説します。

設置フォルダ

今回の自作ブロックは「プラグイン」として制作しました。
このため以下のフォルダに対してファイルを設置します。

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

★印が設置するコードです。

以下で実装コードを示します。

ソース

blocks.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: '' },
            preview: { type: 'object', default: null }
        },

        edit: function( props ) {

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

            return el( 'div', {},
                el( TextControl, {
                    label: 'URLを入力',
                    value: props.attributes.url,
                    onChange: function( value ) {
                        props.setAttributes( { url: value } );
                        if ( value ) {
                            fetchPreview( value );
                        }
                    }
                }),

                props.attributes.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'
                        }
                    },
                
                    // 画像エリア
                    props.attributes.preview.image &&
                    el( 'div',
                        {
                            style: {
                                flex: '0 0 120px',
                                backgroundColor: '#f5f5f5'
                            }
                        },
                        el( 'img', {
                            src: props.attributes.preview.image,
                            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'
                                }
                            },
                            props.attributes.preview.title
                        ),
                        el( 'div',
                            {
                                style: {
                                    fontSize: '12px',
                                    color: '#555',
                                    lineHeight: '1.4'
                                }
                            },
                            props.attributes.preview.description
                        ),
                        el( 'div',
                            {
                                style: {
                                    fontSize: '11px',
                                    color: '#888',
                                    marginTop: '6px'
                                }
                            },
                            new URL(props.attributes.url).hostname
                        )
                    )
                )

            );
        },

        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 );
    $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( $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;
}

 

プラグインの追加

コードの設置が終わると、「プラグイン」の中に
  URL Banner Block
が出現しているはずです。これを「有効化」します。

ブロックエディタ記事編集中に+を押して、「URL」で検索をすればブロック「URLリンクバナー」が見つかるよういなるはず。あとはそれを利用するだけです。

テーマを選ばず、汎用的なリンクバナーとして利用可能なはず。
ソースをいじればカスタマイズも可能。おすすめです。

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

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

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

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

コメントを残す

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

投稿者

KeroYon

関連投稿

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

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

固定ページでDIYラウドスピーカーの記事を

WordPressのメリットは、通常のBlog記事のほかに固定ページも作れるところです。 まあ、簡単...

公開済み記事がやっとこさ半分…?

先月私はWordPressへの記事移行を完遂しました。 ただ、それは記事がフルオープンにできたという...

ブログテーマのスティッキーを封じ込める!

Sticky とはスクロールに追従せず固定される表示のUI用語です。 私のブログは現在、[ Cont...

アメブロ -> WordPress 完全移行、自動化達成

[註] このページは不定期でメンテナンスが入ります 完全自動化プロセスによるCMS移行を実現 私は2...