テクニック

ブログカードの作り方|プラグインなしでWordPressに実装する手順

この記事の対象: WordPress のテーマファイルを編集できる制作者・サイト運営者
読了時間: 約12分

記事の途中に「アイキャッチ画像・タイトル・抜粋文つきのリンク」を貼りたい。いわゆるブログカードと呼ばれるUIで、内部リンクのクリック率を上げる定番の手法です。プラグインで実装する方法もありますが、ショートコード1つとCSSだけで十分動きます。この記事では、WordPress のクラシックテーマ環境で、URLを渡すだけでブログカードを出力するショートコードを一から作ります。


前提条件と完成形の確認

前提条件と完成形の確認

この記事の前提

  • WordPress 6.0 以降
  • クラシックテーマ(functions.php を編集できる構成)
  • 子テーマを使用中、または自作テーマで作業している
  • FTPまたはサーバーのファイルマネージャーでテーマファイルを直接編集できる

ブロックテーマ(FSE)の場合、functions.php の仕組みは同じですが、ブロックエディタ側でショートコードブロックを挿入して使います。

完成形

投稿の本文に次のように書くと、ブログカードが表示されます。

[blogcard url="https://example.com/your-post/"]

出力されるHTML構造はこうなります。

<a class="blogcard" href="https://example.com/your-post/">
  <figure class="blogcard__thumb">
    <img src="アイキャッチ画像のURL" alt="記事タイトル" loading="lazy">
  </figure>
  <div class="blogcard__body">
    <p class="blogcard__title">記事タイトル</p>
    <p class="blogcard__excerpt">抜粋文(最大80文字)</p>
  </div>
</a>

アイキャッチ・タイトル・抜粋の3点を自動で取得し、1枚のカードとして表示します。

プラグインを使わない理由

ブログカード系のプラグインは、oEmbed の上書きやキャッシュテーブルの追加など、裏側で多くの処理を走らせます。内部リンクのカード表示だけが目的なら、ショートコード1つで足ります。プラグインを1つ減らせば、更新管理の手間が減り、プラグイン同士の競合リスクも下がります。

テーマを変更する場合はショートコードごと移植する必要がありますが、functions.php の該当箇所をコピーするだけなので手間は大きくありません。


ショートコードのPHP実装

ショートコードのPHP実装

functions.php に追加するコード

子テーマの functions.php(例: wp-content/themes/your-child-theme/functions.php)の末尾に、次のコードをそのまま貼り付けます。

/**
 * ブログカード用ショートコード
 * 使い方: [blogcard url="記事URL"]
 */
function blogcard_shortcode( $atts ) {
    $atts = shortcode_atts( array(
        'url' => '',
    ), $atts, 'blogcard' );

    $url = esc_url( $atts['url'] );
    if ( empty( $url ) ) {
        return '';
    }

    // 内部URLかどうかを判定
    $site_url = home_url();
    $is_internal = ( strpos( $url, $site_url ) === 0 );

    if ( $is_internal ) {
        // 内部リンク: 投稿IDからデータを取得
        $post_id = url_to_postid( $url );
        if ( ! $post_id ) {
            return '<a href="' . $url . '">' . $url . '</a>';
        }
        $card_title   = get_the_title( $post_id );
        $card_excerpt = get_blogcard_excerpt( $post_id );
        $card_image   = get_blogcard_thumbnail( $post_id );
    } else {
        // 外部リンク: OGP情報を取得
        $ogp = get_blogcard_ogp( $url );
        if ( ! $ogp ) {
            return '<a href="' . $url . '" target="_blank" rel="noopener">' . $url . '</a>';
        }
        $card_title   = $ogp['title'];
        $card_excerpt = $ogp['description'];
        $card_image   = $ogp['image'];
    }

    $target = $is_internal ? '' : ' target="_blank" rel="noopener"';
    $domain_label = $is_internal ? '' : '<span class="blogcard__domain">' . esc_html( wp_parse_url( $url, PHP_URL_HOST ) ) . '</span>';

    ob_start();
    ?>
    <a class="blogcard" href="<?php echo $url; ?>"<?php echo $target; ?>>
        <figure class="blogcard__thumb">
            <?php if ( $card_image ) : ?>
                <img src="<?php echo esc_url( $card_image ); ?>" alt="<?php echo esc_attr( $card_title ); ?>" loading="lazy" width="240" height="160">
            <?php else : ?>
                <span class="blogcard__noimage">No Image</span>
            <?php endif; ?>
        </figure>
        <div class="blogcard__body">
            <p class="blogcard__title"><?php echo esc_html( $card_title ); ?></p>
            <p class="blogcard__excerpt"><?php echo esc_html( $card_excerpt ); ?></p>
            <?php echo $domain_label; ?>
        </div>
    </a>
    <?php
    return ob_get_clean();
}
add_shortcode( 'blogcard', 'blogcard_shortcode' );

抜粋文を取得する関数

同じ functions.php に追加します。投稿に抜粋が設定されていればそれを使い、なければ本文の先頭80文字を切り出します。

/**
 * ブログカード用の抜粋文を取得
 */
function get_blogcard_excerpt( $post_id ) {
    $post = get_post( $post_id );
    if ( ! $post ) {
        return '';
    }
    $excerpt = $post->post_excerpt;
    if ( empty( $excerpt ) ) {
        $excerpt = wp_strip_all_tags( $post->post_content );
    }
    return mb_substr( $excerpt, 0, 80, 'UTF-8' );
}

アイキャッチ画像を取得する関数

アイキャッチが設定されていない投稿は、No Image の代替表示にフォールバックします。

/**
 * ブログカード用のサムネイルURLを取得
 */
function get_blogcard_thumbnail( $post_id ) {
    if ( has_post_thumbnail( $post_id ) ) {
        $thumb = wp_get_attachment_image_src( get_post_thumbnail_id( $post_id ), 'medium' );
        return $thumb ? $thumb[0] : '';
    }
    return '';
}

外部URLのOGP取得(キャッシュ付き)

外部サイトのURLが渡された場合、相手ページの og:title・og:description・og:image を取得します。毎回HTTPリクエストを飛ばすと表示が遅くなるため、WordPress の Transient API で取得結果を24時間キャッシュし、2回目以降はデータベースから読み出します。

/**
 * 外部URLからOGP情報を取得(transientキャッシュ付き)
 */
function get_blogcard_ogp( $url ) {
    $cache_key = 'blogcard_' . md5( $url );
    $cached    = get_transient( $cache_key );

    if ( $cached !== false ) {
        // 取得失敗のキャッシュならfalseを返す
        return $cached === 'error' ? false : $cached;
    }

    $response = wp_remote_get( $url, array(
        'timeout'   => 5,
        'sslverify' => false,
    ) );

    if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
        // 取得失敗も1時間キャッシュして連続リクエストを防ぐ
        set_transient( $cache_key, 'error', HOUR_IN_SECONDS );
        return false;
    }

    $body = wp_remote_retrieve_body( $response );
    $ogp  = array(
        'title'       => '',
        'description' => '',
        'image'       => '',
    );

    // title タグをフォールバック用に取得
    if ( preg_match( '/<title>([^<]*)<\/title>/i', $body, $m ) ) {
        $ogp['title'] = mb_substr( trim( $m[1] ), 0, 80, 'UTF-8' );
    }

    // OGPメタタグを取得(titleタグより優先)
    if ( preg_match( '/property=["\']og:title["\']\s+content=["\']([^"\']*)/i', $body, $m ) ) {
        $ogp['title'] = mb_substr( trim( $m[1] ), 0, 80, 'UTF-8' );
    }
    if ( preg_match( '/property=["\']og:description["\']\s+content=["\']([^"\']*)/i', $body, $m ) ) {
        $ogp['description'] = mb_substr( trim( $m[1] ), 0, 120, 'UTF-8' );
    }
    if ( preg_match( '/property=["\']og:image["\']\s+content=["\']([^"\']*)/i', $body, $m ) ) {
        $ogp['image'] = esc_url( trim( $m[1] ) );
    }

    if ( empty( $ogp['title'] ) ) {
        set_transient( $cache_key, 'error', HOUR_IN_SECONDS );
        return false;
    }

    // 取得成功は24時間キャッシュ
    set_transient( $cache_key, $ogp, DAY_IN_SECONDS );
    return $ogp;
}

キャッシュ管理とパフォーマンスの注意点

キャッシュ管理とパフォーマンスの注意点

キャッシュの仕組み

上の get_blogcard_ogp 関数は、WordPress の Transient API を使ってOGP取得結果を一時保存しています。キャッシュの挙動は次のとおりです。

状況 キャッシュの保持期間
OGP取得に成功した 24時間(DAY_IN_SECONDS)
OGP取得に失敗した 1時間(HOUR_IN_SECONDS)
キャッシュの保存先 wp_options テーブル

取得失敗時も短時間キャッシュする理由は、相手サーバーへの連続リクエストを避けるためです。1時間経てば自動で再取得を試みます。

キャッシュの手動クリア

相手サイトがOGP情報を更新した場合など、キャッシュを手動で消したい場面があります。方法は2つです。

  1. phpMyAdmin や WP-CLI から直接削除する — wp_options テーブルで option_name が _transient_blogcard_ で始まる行を検索し、削除する
  2. WP-CLI を使う — wp transient delete --all で全 Transient を一括削除できる(ブログカード以外のキャッシュも消える点に注意)

特定のURLのキャッシュだけを消したい場合は、wp transient delete blogcard_<URLのmd5ハッシュ> を実行します。

外部カードの枚数と表示速度

内部リンクのカードは WordPress のデータベースから直接取得するため、枚数が増えても表示速度への影響はほとんどありません。一方、外部リンクのカードは初回表示時にHTTPリクエストが発生します。1ページあたりの外部カードは3枚以内に抑えるのが現実的です。


CSSでブログカードをレイアウトする

CSSでブログカードをレイアウトする

スタイルの追加場所

子テーマの style.css(例: wp-content/themes/your-child-theme/style.css)の末尾に追加します。テーマのカスタマイザーで「追加CSS」に書いても構いません。

基本のCSS

/* ブログカード */
.blogcard {
  display: flex;
  gap: 16px;
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 16px;
  margin: 24px 0;
  text-decoration: none;
  color: inherit;
  background: #fff;
  transition: box-shadow 0.2s;
}

.blogcard:hover {
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}

/* サムネイル */
.blogcard__thumb {
  flex-shrink: 0;
  width: 160px;
  margin: 0;
}

.blogcard__thumb img {
  width: 100%;
  height: 100px;
  object-fit: cover;
  border-radius: 4px;
}

/* アイキャッチなしの場合 */
.blogcard__noimage {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100px;
  background: #f0f0f0;
  border-radius: 4px;
  font-size: 12px;
  color: #999;
}

/* テキスト部分 */
.blogcard__body {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-width: 0;
}

.blogcard__title {
  font-size: 15px;
  font-weight: 700;
  line-height: 1.5;
  margin: 0 0 6px;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

.blogcard__excerpt {
  font-size: 13px;
  color: #666;
  line-height: 1.6;
  margin: 0;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

/* 外部リンクのドメイン表示 */
.blogcard__domain {
  font-size: 11px;
  color: #999;
  margin-top: 6px;
}

/* スマホ対応 */
@media (max-width: 600px) {
  .blogcard {
    flex-direction: column;
    gap: 12px;
  }

  .blogcard__thumb {
    width: 100%;
  }

  .blogcard__thumb img,
  .blogcard__noimage {
    height: 160px;
  }
}

CSSの調整ポイント

読者自身のサイトに合わせて変更が必要になりやすい箇所をまとめます。

プロパティ 初期値 変更の目安
.blogcard の border 1px solid #ddd サイトのカラーに合わせる
.blogcard の border-radius 8px 角丸を使わないデザインなら 0
.blogcard__thumb の width 160px カード全体の幅に対して調整
.blogcard__title の font-size 15px 本文の文字サイズに揃える
@media のブレークポイント 600px テーマのブレークポイントに合わせる

使い方と表示の確認

使い方と表示の確認

基本の使い方

投稿の編集画面で、ブログカードを表示したい位置に次のショートコードを書きます。

[blogcard url="https://あなたのサイト.com/投稿のURL/"]

ブロックエディタの場合は「ショートコード」ブロックを追加し、その中に書いてください。クラシックエディタの場合はそのまま本文に記述します。

内部リンクと外部リンクの挙動の違い

項目 内部リンク 外部リンク
データ取得元 WordPress のデータベース 相手サイトの HTML(OGP)
表示速度 即座 初回は数秒、2回目以降はキャッシュ
target 属性 なし(同一タブ) _blank(新しいタブ)
ドメイン表示 なし カード下部にドメインを表示
画像なしの場合 「No Image」を表示 OGP画像がなければ「No Image」

外部リンクで target="_blank" を付けたくない場合は、PHPの $target 変数の分岐を削除してください。

下書きや非公開記事のカード

url_to_postid() は公開済みの記事のみ投稿IDを返します。下書きや非公開の記事URLを指定した場合、IDが取得できずプレーンなテキストリンクとして出力されます。これは意図した挙動で、公開前の記事がカードとして露出することを防ぎます。


ファイル構成のまとめ

ファイル構成のまとめ

今回編集するファイルは2つだけです。

編集対象のファイル

wp-content/themes/your-child-theme/

  • functions.php(ショートコード+取得関数4つ)
  • style.css(カードのレイアウト)

your-child-theme は実際の子テーマ名に置き換えてください。

functions.php に追加する関数は次の4つです。

  • blogcard_shortcode — ショートコード本体
  • get_blogcard_excerpt — 抜粋文の取得
  • get_blogcard_thumbnail — アイキャッチ画像の取得
  • get_blogcard_ogp — 外部URLのOGP取得(キャッシュ付き)

保守のしやすさを考えると、これらをまとめて functions.php 内でコメントで区切っておくと、テーマ変更時の移植がスムーズです。


うまくいかないとき

うまくいかないとき

カードが表示されずショートコードがそのまま出る

functions.php の構文エラーが原因です。PHPの開始タグ <?php が重複していないか、関数の閉じカッコ } が抜けていないかを確認してください。エラーの内容は、wp-content/debug.log に記録されます(wp-config.php で WP_DEBUG と WP_DEBUG_LOG を true にしている場合)。

内部リンクなのにプレーンなリンクになる

url_to_postid() が0を返しています。原因として多いのは次の3つです。

  • パーマリンク設定を変更した直後で、リライトルールが更新されていない → 管理画面の「設定 > パーマリンク」を開いて何も変えずに保存する
  • URLの末尾スラッシュの有無が一致していない → https://example.com/post と https://example.com/post/ は別物として扱われる場合がある
  • カスタム投稿タイプの記事で、public が false になっている

外部リンクの画像が表示されない

相手サイトが og:image を設定していないケースです。「No Image」の代替表示が出ていれば正常動作です。相手サイトがリクエストをブロックしている場合も同様の結果になります。wp_remote_get のタイムアウトを延ばしても改善しない場合、相手サーバー側の制限なので対処は難しいです。

スマホで画像が大きすぎる・小さすぎる

CSSの @media (max-width: 600px) のブレークポイントがテーマと合っていない可能性があります。テーマのレスポンシブ切り替え幅を確認し、数値を揃えてください。


よくある質問

よくある質問

ブログカードはSEOに影響しますか?

ブログカード自体がSEOの評価を上げることはありません。出力されるのは通常の <a> タグなので、検索エンジンには普通の内部リンクとして認識されます。ただし、視覚的にリンクが目立つことでクリック率が上がり、ユーザーの回遊が増える効果は期待できます。

クラシックエディタでも使えますか?

使えます。ショートコードはエディタの種類を問わず動作します。クラシックエディタの場合、ビジュアルモードでもテキストモードでも [blogcard url="..."] をそのまま書けば表示されます。

固定ページのURLでもカード表示できますか?

できます。url_to_postid() は投稿・固定ページ・カスタム投稿タイプすべてに対応しています。固定ページのURLを渡せば、そのページのアイキャッチとタイトルがカードに表示されます。

外部リンクのカードにファビコンを表示したいときは?

PHP側でファビコン取得用のAPIを利用すれば画像を取得できますが、外部サービスに依存する形になります。安定性を重視するなら省略しておくほうが無難です。表示したい場合は、OGP取得時にあわせて <link rel="icon"> タグを正規表現で抜き出し、ドメイン表示の横にファビコン画像を追加する方法が自前で完結します。

テーマを変えたらカードが消えますか?

functions.php に書いたショートコードはテーマに紐づいているため、テーマを変更すると動かなくなります。新しいテーマの functions.php に同じコードをコピーすれば復旧します。本文中のショートコード記述 [blogcard url="..."] はデータベースに残っているので、書き直す必要はありません。


まとめ

ブログカードの作り方は、functions.php への関数追加と style.css へのレイアウト記述だけで完結します。内部リンクは url_to_postid() でデータベースから直接取得し、外部リンクは wp_remote_get でOGP情報を引いて Transient でキャッシュする。この仕組みを理解しておけば、表示のカスタマイズも自由にできます。

プラグインに頼らず自前で実装することで、不要な機能を読み込まずに済み、テーマの構成もシンプルに保てます。コードをコピーして functions.php に貼り、CSSを追加すれば、今日のうちにブログカードの実装は終わります。