URLを書くだけで外部サイトをリンクカード表示

外部サイトの URL を書くだけで、リンク先のページのタイトルを取得して、API で読み込んだファビコンと合わせてカードにする。ショートコードでも、ブロックエディタの「+」からでも使える。

REFERENCE“STRAIGHT FROM THE SOURCE” WP-PageNavi – WordPress プラグイン ja.wordpress.org

カードに表示されるもの

1行目:アイコン・REFERENCE・コメント("STRAIGHT FROM THE SOURCE")
2行目:リンク先のページのタイトル(og:title → <title> の順に自動で取る)
3行目:リンク先のドメイン 2・3行目の左;リンク先のファビコン(2行分の高さ・グレー)
※ カード全体がリンクで、別タブで開く。マウスオンでタイトルとドメインが薄くなる。

WordPress 用。ブロックを使う場合は WordPress 6.3 以上。アイコンに Font Awesome 6 以上を使う(読み込んでいない場合は「Font Awesome を読み込んでいない場合」を参照)。ビルド(npm)は不要。

ファイルの構成

テーマ(子テーマを使っている場合は子テーマ)に3つのファイルを格納する。

ファイル格納先役割
functions.phpテーマの functions.php に追記タイトルを取得して、API で読み込んだファビコンと合わせてカードの HTML を作る。ショートコードとブロックの登録
reference-card.cssテーマの css/ フォルダデザイン
reference-block.jsテーマの js/ フォルダブロックエディタの入力欄(ブロックを使わない場合は不要)

※ ファイルの格納先は get_theme_file_uri() で探すので、子テーマに格納しても親テーマに格納しても動く。 get_theme_file_uri() で探すので、子テーマに格納しても親テーマに格納しても動く。

functions.php

/* =====================================================================
   外部リンクのカード REFERENCE(ショートコード [reference] + ブロック「REFERENCE(外部リンク)」)
   ・URL を書くだけで、リンク先のタイトルを取得して、API で読み込んだファビコンと合わせてカードにする
   ・出力の形:
       ┃ ↗ REFERENCE "STRAIGHT FROM THE SOURCE"
       ┃ [ファビコン] ページのタイトル
       ┃ [  2行分  ] EXAMPLE.COM
   ・デザイン:css/reference-card.css ブロック(編集画面):js/reference-block.js
   ===================================================================== */

// REFERENCE の横のひと言(変えるときはここだけ)
define( 'REFCARD_TAGLINE', '"STRAIGHT FROM THE SOURCE"' );

/**
 * 外部ページのタイトルを取る(og:title → <title> の順)
 *
 * ・表示のたびに相手のサイトへ行くと重いので transient で控える(取れた:7日/取れなかった:1日で取り直し)
 * ・wp_safe_remote_get を使う(localhost や社内ネットワークへのアクセスは WordPress 側で止まる)
 * ・読むのは先頭 200KB まで(タイトルは <head> にあるので足りる)
 *
 * @param string $url 外部ページの URL。
 * @return string タイトル(取れなければ空文字)。
 */
function refcard_get_title( $url ) {
	$cache_key = 'refcard_' . md5( $url );
	$cached    = get_transient( $cache_key );
	if ( false !== $cached ) {
		return (string) $cached;
	}

	$title    = '';
	$response = wp_safe_remote_get(
		$url,
		array(
			'timeout'             => 5,
			'redirection'         => 3,
			'limit_response_size' => 200 * 1024,
			'user-agent'          => 'Mozilla/5.0 (compatible; WordPress; ' . home_url() . ')',
		)
	);

	if ( ! is_wp_error( $response ) && 200 === (int) wp_remote_retrieve_response_code( $response ) ) {
		$html = (string) wp_remote_retrieve_body( $response );

		// 文字コード:UTF-8 以外(Shift_JIS・EUC-JP など)のページは UTF-8 に直してから読む
		if ( preg_match( '/<meta[^>]+charset=["\']?([\w-]+)/i', $html, $cm ) && 'utf-8' !== strtolower( $cm[1] ) && function_exists( 'mb_convert_encoding' ) ) {
			$converted = @mb_convert_encoding( $html, 'UTF-8', $cm[1] ); // phpcs:ignore WordPress.PHP.NoSilencedErrors -- 知らない文字コード名のときは変換しない
			if ( false !== $converted ) {
				$html = $converted;
			}
		}

		// og:title(property と content の順番が逆のページもあるので両方見る)→ なければ <title>
		if ( preg_match( '/<meta[^>]+property=["\']og:title["\'][^>]+content=["\']([^"\']*)["\']/i', $html, $m )
			|| preg_match( '/<meta[^>]+content=["\']([^"\']*)["\'][^>]+property=["\']og:title["\']/i', $html, $m ) ) {
			$title = $m[1];
		} elseif ( preg_match( '/<title[^>]*>(.*?)<\/title>/is', $html, $m ) ) {
			$title = $m[1];
		}

		// 文字参照(& など)を戻して、改行・余分な空白を1つに
		$title = trim( preg_replace( '/\s+/u', ' ', html_entity_decode( wp_strip_all_tags( $title ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ) ) );
	}

	set_transient( $cache_key, $title, '' !== $title ? WEEK_IN_SECONDS : DAY_IN_SECONDS );
	return $title;
}

/**
 * REFERENCE カードの HTML を作る(ショートコード・ブロック共通)
 *
 * @param array $atts url(必須)/title(タイトルを自分で決めたいとき)。
 * @return string HTML
 */
function refcard_render( $atts ) {
	$atts = shortcode_atts(
		array(
			'url'   => '',
			'title' => '',
		),
		$atts,
		'reference'
	);

	// URL は http:// か https:// で始まり、ドメインがあるものだけ(空・おかしな URL なら何も出さない)
	$url = trim( $atts['url'] );
	if ( ! preg_match( '#^https?://#i', $url ) ) {
		return '';
	}
	$url  = esc_url_raw( $url, array( 'http', 'https' ) );
	$host = (string) wp_parse_url( $url, PHP_URL_HOST );
	if ( '' === $host ) {
		return '';
	}

	// タイトル:指定 → 相手のページから取ったタイトル → ドメイン名 の順
	$title = sanitize_text_field( $atts['title'] );
	if ( '' === $title ) {
		$title = refcard_get_title( $url );
	}
	if ( '' === $title ) {
		$title = $host;
	}

	// ファビコン(Google の favicon API。64px で取って小さく出す=高解像度の画面でもぼやけない)
	// ・<img> ではなく <span> の背景画像で出す(テーマの「記事内の画像」の指定で大きさ・位置が変わらないように)
	$favicon = add_query_arg(
		array(
			'domain' => rawurlencode( $host ),
			'sz'     => 64,
		),
		'https://www.google.com/s2/favicons'
	);

	return '<a class="refcard" href="' . esc_url( $url ) . '" target="_blank" rel="noopener noreferrer">'
		. '<span class="refcard__head"><i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i><span class="refcard__label">REFERENCE</span><span class="refcard__tagline">' . esc_html( REFCARD_TAGLINE ) . '</span></span>'
		. '<span class="refcard__row">'
		. '<span class="refcard__fav" style="background-image: url(\'' . esc_url( $favicon ) . '\');" aria-hidden="true"></span>'
		. '<span class="refcard__text">'
		. '<span class="refcard__name">' . esc_html( $title ) . '</span>'
		. '<span class="refcard__host">' . esc_html( preg_replace( '/^www\./', '', $host ) ) . '</span>'
		. '</span>'
		. '</span>'
		. '</a>';
}
add_shortcode( 'reference', 'refcard_render' );

/**
 * デザイン(css/reference-card.css)を、公開ページと編集画面(ブロックのプレビュー)の両方で読み込む
 * ・enqueue_block_assets フックは、公開ページとブロックエディタの中の両方で動く
 */
function refcard_enqueue_style() {
	wp_enqueue_style(
		'refcard',
		get_theme_file_uri( 'css/reference-card.css' ),
		array(),
		filemtime( get_theme_file_path( 'css/reference-card.css' ) ) // 更新したら自動でキャッシュ更新
	);
}
add_action( 'enqueue_block_assets', 'refcard_enqueue_style' );

/**
 * ブロック「REFERENCE(外部リンク)」を登録(ブロックを使わない場合は、下の add_action の1行をコメントアウト)
 * ・表示は refcard_render()(保存するのは URL とタイトルの設定だけ。表示は毎回 PHP で作る)
 * ・編集画面の入力欄:js/reference-block.js(ビルドなし)
 */
function refcard_register_block() {
	wp_register_script(
		'refcard-block',
		get_theme_file_uri( 'js/reference-block.js' ),
		array( 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-server-side-render' ),
		filemtime( get_theme_file_path( 'js/reference-block.js' ) ),
		true
	);

	register_block_type(
		'refcard/reference',
		array(
			'api_version'     => 3,
			'editor_script'   => 'refcard-block',
			'attributes'      => array(
				'url'   => array( 'type' => 'string', 'default' => '' ),
				'title' => array( 'type' => 'string', 'default' => '' ),
			),
			'render_callback' => 'refcard_render',
		)
	);
}
add_action( 'init', 'refcard_register_block' );

※ タイトルは表示のたびに取得せず、7日間保存しておく。表示のたびにリンク先のサイトから取得すると、ページの表示が遅くなり、リンク先のサイトにも負担がかかるため。取得できなかったときは、リンク先のサイトが一時的に止まっていた場合に備えて、1日で取得し直す。
※ アクセスを断るサイトなど、タイトルを取得できない場合は、タイトルの位置にドメイン名を表示する。title を書けば、自分で決めたタイトルにできる。
※ ファビコンは Google の favicon API(https://www.google.com/s2/favicons)から読み込む。

CSS

テーマの css/ フォルダに reference-card.css の名前で保存。読み込みは functions.php で済ませてあるので、header.php などに書き足す必要はない。

/* =====================================================================
   外部リンクのカード REFERENCE(functions.php の refcard_render())
   ・左に縦線、1行目「↗ REFERENCE "STRAIGHT FROM THE SOURCE"」、2・3行目にファビコンとタイトル・ドメイン
   ・カード全体がリンク(別タブ)。マウスオンでタイトル・ドメインが薄くなる
   ===================================================================== */
a.refcard,
a.refcard:link,
a.refcard:visited {
  display: block;
  margin: 24px 0;
  padding: 2px 0 2px 14px;
  border: 0;                        /* テーマがリンクに設定している線(下線など)を消してから */
  border-left: 3px solid #bcbcbc;   /* 縦線 */
  color: #3c3c3b;                   /* 文字の色 */
  text-decoration: none;            /* テーマのリンクの下線を打ち消す */
  box-shadow: none;
}

/* 1行目:アイコン REFERENCE "ひと言" */
.refcard__head {
  display: block;
  font-size: 12px;
  line-height: 1.3;
}
.refcard__head i {
  color: #bcbcbc;
  margin-right: 0.4em;
}
.refcard__label,
.refcard__tagline {
  font-family: 'Montserrat', sans-serif;
  font-weight: 700;
  letter-spacing: 0;
  text-transform: uppercase;
}
.refcard__tagline {
  margin-left: 0.6em;
}

/* 2・3行目:左にファビコン(2行分の高さ)、右にタイトル・ドメイン */
.refcard__row {
  display: flex;
  align-items: center;
  gap: 8px;                  /* ファビコン 〜 文字 */
  margin-top: 3px;
}
.refcard__fav {
  flex: 0 0 auto;
  display: block;
  width: 28px;
  height: 28px;
  background: no-repeat center / cover;   /* 画像は functions.php で style に入れている */
  border-radius: 4px;        /* 少しだけ角丸(アイコンっぽく) */
}
/* ファビコンをグレーに(カラーのままにするときは、このまとまりをコメントアウト) */
.refcard__fav {
  filter: grayscale(1);
  opacity: .8;               /* 濃さ(1 で元の濃さ) */
}
.refcard__text {
  display: block;
  min-width: 0;
}
.refcard__name {
  display: block;
  font-size: 12px;
  line-height: 1.3;
  overflow-wrap: anywhere;   /* 長い英語のタイトルでもはみ出さない */
}
.refcard__host {
  display: block;
  margin-top: 3px;
  font-family: 'Montserrat', sans-serif;
  font-size: 10.8px;
  font-weight: 700;
  line-height: 1.2;
  text-transform: uppercase;
}

/* マウスオン:タイトル・ドメインを薄く */
a.refcard:hover .refcard__name,
a.refcard:hover .refcard__host {
  color: #bdc3c7;
}

※ テーマ側でリンクに下線や線を設定している場合に備えて、text-decoration: none と border: 0 で消してから、左の縦線を引いている。
※ ファビコンは <img> ではなく <span> の背景画像で表示している。テーマで記事内の画像に大きさや中央寄せを指定している場合も、ファビコンの大きさ・位置は変わらない。
※ 公開画面とブロックエディタのプレビューの両方で、同じ CSS を読み込む。

JavaScript

テーマの js/ フォルダに reference-block.js の名前で保存。ブロックエディタに「REFERENCE(外部リンク)」ブロックを足すためのファイルで、公開画面では読み込まない。

/* =====================================================================
   ブロック「REFERENCE(外部リンク)」:記事の編集画面で URL を入力するだけで外部リンクのカードを作る
   - 表示(公開ページ・編集画面のプレビュー)は functions.php の refcard_render()(ショートコード [reference] と同じ)
   - ブロックの中:URL の入力欄 + 入力欄の下にプレビュー
   - 右の設定パネル:タイトル(空なら相手のページから自動)
   - ショートコード [reference …] からブロックに変換できる(段落に貼り付けたとき・クラシックブロックの「ブロックへ変換」のとき)
   - ビルド(npm)なしで動くように、WordPress に入っている部品を素の JavaScript で使う
   ===================================================================== */
( function ( wp ) {
	'use strict';

	var el                = wp.element.createElement;
	var Fragment          = wp.element.Fragment;
	var registerBlockType = wp.blocks.registerBlockType;
	var useBlockProps     = wp.blockEditor.useBlockProps;
	var InspectorControls = wp.blockEditor.InspectorControls;
	var PanelBody         = wp.components.PanelBody;
	var TextControl       = wp.components.TextControl;
	var Disabled          = wp.components.Disabled;
	var ServerSideRender  = wp.serverSideRender;

	registerBlockType( 'refcard/reference', {
		apiVersion: 3,
		title: 'REFERENCE(外部リンク)',
		description: 'URL を貼るだけで、外部リンクのカードを作る',
		category: 'widgets',
		icon: 'external',
		keywords: [ 'reference', '外部リンク', 'リンクカード', 'url' ],
		attributes: {
			url:   { type: 'string', default: '' },
			title: { type: 'string', default: '' }
		},
		supports: { html: false },

		// ショートコード [reference url="…" title="…"] → ブロックに変換
		// ・段落に貼り付けると自動でブロックになる/クラシックブロックの「ブロックへ変換」でもなる
		// ・ショートコードブロックの「変換」メニューには出ない(WordPress の仕様)
		transforms: {
			from: [
				{
					type: 'shortcode',
					tag: 'reference',
					attributes: {
						url:   { type: 'string', shortcode: function ( a ) { return a.named.url || ''; } },
						title: { type: 'string', shortcode: function ( a ) { return a.named.title || ''; } }
					}
				}
			]
		},

		edit: function ( props ) {
			var a          = props.attributes;
			var blockProps = useBlockProps();

			return el( Fragment, null,
				el( InspectorControls, null,
					el( PanelBody, { title: 'REFERENCE の設定', initialOpen: true },
						el( TextControl, {
							label: 'タイトル(空なら相手のページから自動)',
							value: a.title,
							onChange: function ( v ) { props.setAttributes( { title: v } ); }
						} )
					)
				),
				el( 'div', blockProps,
					el( TextControl, {
						label: 'REFERENCE:リンク先の URL',
						type: 'url',
						placeholder: 'https://',
						value: a.url,
						onChange: function ( v ) { props.setAttributes( { url: v } ); }
					} ),
					a.url
						? el( Disabled, null, el( ServerSideRender, { block: 'refcard/reference', attributes: a } ) )
						: null
				)
			);
		},

		// 保存しない(表示は毎回 PHP で作る)
		save: function () { return null; }
	} );
} )( window.wp );

使い方

ショートコードは、URL だけ書けばタイトルを自動で取得する。

[reference url="https://ja.wordpress.org/plugins/wp-pagenavi/"]

タイトルを自分で決める場合は title を指定する。

[reference url="https://ja.wordpress.org/plugins/wp-pagenavi/" title="表示したいタイトル"]

ブロックの場合

  1. 投稿の編集画面で「+」→「REFERENCE(外部リンク)」
  1. 「REFERENCE:リンク先の URL」に URL を入力する → 下にカードのプレビューが表示される
  1. タイトルを変える場合は、右の設定パネル「REFERENCE の設定」→「タイトル(空なら相手のページから自動)」に入力

※ 段落に [reference url="…"] を貼り付けると、自動で REFERENCE ブロックになる。ショートコードのまま残す場合は「ショートコード」ブロックに書く。
※ クラシックブロックの中の [reference …] は、「ブロックへ変換」で REFERENCE ブロックになる。「ショートコード」ブロックの「変換」メニューには表示されない

調整できる部分の設定

変えたいもの変更箇所
REFERENCE の横のひと言functions.php の REFCARD_TAGLINE
ファビコンをカラーにするCSS の「ファビコンをグレーに」のまとまりをコメントアウト
ファビコンの濃さCSS の opacity(1 で元の濃さ)
タイトルを保存しておく日数functions.php の WEEK_IN_SECONDS(取得できた場合の7日)・DAY_IN_SECONDS(取得できなかった場合の1日)
文字の色CSS の a.refcard の color
縦線・アイコンの色CSS の border-left と .refcard__head i の color
マウスオンの色CSS の a.refcard:hover の color

※ 保存しておいたタイトルは、日数が過ぎるまで変わらない。リンク先のタイトルが変わった場合も、日数が過ぎてから新しいタイトルになる。

ブロックを使わない場合

ショートコードだけで使う場合は、functions.php の最後の1行をコメントアウトして、reference-block.js は格納しない。

// add_action( 'init', 'refcard_register_block' );

※ 1行を残したまま reference-block.js を格納しないと、ファイルの更新日時を読めずに PHP の Warning になる。

Font Awesome

テーマで Font Awesome 6 以上を読み込んでいない場合は、functions.php に下記を追記(cdnjs の Font Awesome 6.7.2)。公開画面とブロックエディタのプレビューの両方で読み込む。

// Font Awesome 6.7.2(cdnjs)を公開画面とブロックエディタの両方で読み込む
function refcard_enqueue_fontawesome() {
	wp_enqueue_style(
		'font-awesome',
		'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css',
		array(),
		'6.7.2'
	);
}
add_action( 'enqueue_block_assets', 'refcard_enqueue_fontawesome' );

※ すでに Font Awesome を読み込んでいる場合は、二重になるので追記しない。

表示環境によって動きの見え方が異なる場合があるため、本番に反映する前に確認してね。

NOTICES

  • 記事内容は実装させたものがほとんどですが自己責任で参考にしてください。