クエリループをショートコードで呼び出す方法|ページビルダー環境でカスタムフィールドまで表示する

あなたのサイト、こんなことになっていませんか?
まめすけクエリループが便利そうなのに、うちのページビルダーだと置く場所がない……
私はフリーランスでWordPressサイトの制作と保守を請け負っています。あるクライアントから「実績ページの見せ方を、参考サイトのように変えられないか」と相談を受けました。
話はその時点で止まったままなのですが、自分の技術でどこまで近づけられるのかを確かめたくて、テスト環境で実装してみました。この記事はその検証記録です。
結論から言うと、実装できました。ただし3回、方針を転換しています。うまくいかなかった経緯まで含めて残します。


何を作ったのか?
カスタムフィールドの内容を表示する、カード型の実績一覧です。
仕様は以下のとおりです。
- 1件につき写真を複数枚登録できるが、一覧では1枚目だけ表示
- 写真をクリックすると、その案件の写真だけがLightboxで見られる
- 工事名称・施工年・施工概要を、ラベル付きの行で表示
- 「公共工事」「民間工事」など、セクションごとに分けて配置できる
使ったプラグインは Custom Post Type UI と Lightbox の2つだけです。ACFは使っていません。
なぜ普通の方法では作れないのか?
壁が2つあるからです。置けないうえに、置けても足りません。
ページビルダーにはクエリループを置けない
このサイトは Themify Builder で構築されています。ページの中身はビルダー独自のモジュールで組み立てる仕組みなので、Gutenbergのクエリループブロックを直接置けません。
これはThemifyに限った話ではありません。Elementor、WPBakery、Diviなど、独自ビルダーを使うテーマ全般で起きる問題です。
クエリループはカスタムフィールドを表示できない
もう一つの壁がこちらです。クエリループが出せるのは、タイトル・日付・抜粋・アイキャッチといった標準の項目だけという仕様になっています。
カスタムフィールドを出したければ、通常はブロックを自作するか、ACFのようなプラグインに頼ることになります。
どこで方針を変えたのか?(3回の試行錯誤)
最初の構想から、3回作り直しています。
転換①|抜粋と公開日では項目が足りなかった
最初はプラグインなしのシンプル構成を狙いました。
- 施工年 → 公開日を施工年月として使う
- 説明文 → 抜粋を使う
- 写真 → 本文のギャラリーブロックをそのまま出す
標準機能だけで完結するので、更新にも強い。悪くない発想だと思っていました。
しかし実際に項目を並べてみると足りません。工事名称は投稿タイトルと別に必要ですし、施工年は「令和6年度」のような和暦の年度表記にしたい。公開日を使うと西暦の日付になってしまいます。
表示したい項目が3つある時点で、標準フィールドの流用には無理がありました。
そこでカスタムフィールドに切り替えました。ただしACFは入れません。WordPress標準のカスタムフィールド機能を使います。
works_name … 工事正式名称
works_year … 施工年度
works_desc … 施工概要
標準のカスタムフィールド欄は、初期状態では表示されていません。投稿編集画面の右上「⋮」→ 設定 → 一般 → 高度な設定「カスタムフィールド」をONにしてください。
なお、この設定はユーザーごとです。クライアントが更新する場合は、その方の環境でもONにする必要があります。


転換②|本文の段落では表示を制御できなかった
説明文は当初、本文に書く想定でした。
投稿画面を開いた時点でギャラリーと段落が用意されるよう、テンプレートを設定していました。
$post_type_object->template = array(
array( 'core/gallery', array( 'linkTo' => 'media' ) ),
array( 'core/paragraph', array( 'placeholder' => '補足説明があれば入力...' ) ),
);
しかしこれだと、一覧ページでの扱いが難しくなります。本文の中にギャラリーと段落が混在しているため、「写真だけ取り出す」「説明文だけ別の場所に置く」といった制御ができません。
結局、説明文も works_desc としてカスタムフィールドに移しました。本文はギャラリー専用の場所になりました。
転換③|投稿タイプ判定では動かなかった
ここが一番ハマった部分です。
カスタムフィールドを表示させるために render_block フィルターを使ったのですが、このフィルターはサイト全体の全ブロックに効いてしまいます。関係ないページの段落まで書き換わる恐れがありました。
そこで最初は、投稿タイプで判定しようとしました。
// これは動きませんでした
if ( 'works' !== get_post_type() ) {
return $block_content;
}
動きません。 理由は明確で、このショートコードが実行されるのは固定ページの上だからです。get_post_type() が返すのは page であって works ではありません。
クエリループの内側では投稿が切り替わりますが、フィルターの発火タイミングと噛み合わず、安定しませんでした。
最終的にたどり着いた解決策は後述します。「場所で判定する」から「時間で判定する」への発想の転換でした。
クエリループをページに置くにはどうする?
パターンとして保存し、ショートコードで呼び出します。
パターンとして保存する
投稿編集画面でクエリループを組み立て、パターンとして保存します。ブロックを選択して「⋮」→「パターンを作成」で保存できます。
クラシックテーマの場合、管理画面に「パターン」のメニューが表示されません。以下のURLで直接開けます。
/wp-admin/edit.php?post_type=wp_block
保存したパターンのIDは、編集画面のURLに含まれる post=○○○○ の数字です。


呼び出し用のショートコードを作る
子テーマの functions.php に追記します。
/**
* パターン(再利用ブロック)をショートコードで呼び出す
*/
function k_suke_display_reusable_block( $atts ) {
$atts = shortcode_atts( array( 'id' => '' ), $atts );
if ( empty( $atts['id'] ) ) {
return '';
}
$post = get_post( $atts['id'] );
if ( ! $post || 'wp_block' !== $post->post_type ) {
return '';
}
// 実績リストを描画中であることを示すフラグを立てる
$GLOBALS['is_works_loop_active'] = true;
// ブロックをHTMLに変換
$content = do_blocks( $post->post_content );
$content = do_shortcode( $content );
// フラグを解除
$GLOBALS['is_works_loop_active'] = false;
return $content;
}
add_shortcode( 'my_block', 'k_suke_display_reusable_block' );
肝は do_blocks() です。ブロックのデータをHTMLに変換する関数で、これを通すことでパターンの中身が実際の表示になります。
$GLOBALS の2行が、転換③で行き着いた解決策です。詳しくは次の章で説明します。
ページビルダーに貼り付ける
Themify Builder の場合、「HTML / Text / Shortcode」モジュールを置いて、以下を記述します。
[my_block id="3330"]
これだけです。ビルダーの好きな位置に置けるので、セクション見出しの下に配置したり、複数のパターンを使い分けたりできます。
私の場合は「公共電気設備工事」と「民間電気設備工事」でパターンを分け、同じページに2つ置いています。





ショートコードにしちゃえば、どんなビルダーでも置けるってことだね
カスタムフィールドはどうやって表示させる?
段落ブロックを「空の器」として置き、中身をPHPで差し替えます。
段落ブロックを「空の器」として使う
パターン側では、値を表示したい位置に段落ブロックを置き、追加CSSクラスを付けておきます。
| クラス | 対応するフィールド |
|---|---|
display-works-name | works_name |
display-works-year | works_year |
display-works-desc | works_desc |
段落の中身は「工事正式名称」のようなダミーテキストで構いません。フロントでは表示されず、実際の値に置き換わります。
フラグで実行区間を限定する
転換③の解決策がこれです。ショートコードの実行中だけフラグを立てます。
$GLOBALS['is_works_loop_active'] = true;
$content = do_blocks( $post->post_content );
$GLOBALS['is_works_loop_active'] = false;
フィルター側では、このフラグだけを見ます。
if ( empty( $GLOBALS['is_works_loop_active'] ) ) {
return $block_content;
}
「どのページか」ではなく「今このショートコードを描画中か」で判定する。 これなら固定ページだろうとどこだろうと確実に効きますし、それ以外の場所には一切影響しません。
実装コード全文
/**
* ブロック出力時にカスタムフィールドを差し込む
*/
function k_suke_render_block_filters( $block_content, $block ) {
// ショートコード描画中でなければ何もしない
if ( empty( $GLOBALS['is_works_loop_active'] ) ) {
return $block_content;
}
// --- A. カスタムフィールドの埋め込み ---
if ( 'core/paragraph' === $block['blockName'] && isset( $block['attrs']['className'] ) ) {
$classes = $block['attrs']['className'];
$post_id = get_the_ID();
$field_name = '';
if ( strpos( $classes, 'display-works-name' ) !== false ) {
$field_name = 'works_name';
} elseif ( strpos( $classes, 'display-works-year' ) !== false ) {
$field_name = 'works_year';
} elseif ( strpos( $classes, 'display-works-desc' ) !== false ) {
$field_name = 'works_desc';
}
if ( $field_name ) {
$val = get_post_meta( $post_id, $field_name, true );
if ( ! empty( $val ) ) {
$display_val = ( 'works_desc' === $field_name )
? nl2br( esc_html( $val ) )
: esc_html( $val );
return preg_replace( '/(<p[^>]*>)(.*?)(<\/p>)/is', '$1' . $display_val . '$3', $block_content );
}
return '';
}
}
// --- B. 画像のLightboxグループ分け ---
if ( 'core/image' === $block['blockName'] ) {
$post_id = get_the_ID();
$pattern = '/<a(.*?)href="([^"]*?\.(?:jpg|jpeg|gif|png|webp))"(.*?)>/i';
$replacement = '<a$1href="$2" data-rel="lightbox-gallery-' . $post_id . '"$3>';
return preg_replace( $pattern, $replacement, $block_content );
}
return $block_content;
}
add_filter( 'render_block', 'k_suke_render_block_filters', 10, 2 );
ポイントを3つ挙げます。
<p>タグの中身だけを置換しているので、段落に設定した文字色や余白はそのまま残ります- 値が空なら
return ''でその行ごと消えます。未入力の項目が空欄で残りません - 説明文だけ
nl2br()を通しているので、改行がそのまま反映されます
Lightboxが全部つながってしまうのはどう防ぐ?
投稿IDをグループ名に埋め込みます。
Lightboxは通常、ページ内の画像をひとまとまりとして扱います。そのままだとA案件の写真を見ていて次に進むと、B案件の写真が出てくることになります。
これを防ぐのが、先ほどのコードの後半です。
$replacement = '<a$1href="$2" data-rel="lightbox-gallery-' . $post_id . '"$3>';
投稿ごとに異なるグループ名になるため、案件ごとに独立したギャラリーとして動作します。
前提として、**ギャラリー画像のリンク先が「メディアファイル」**になっている必要があります。投稿テンプレートで 'linkTo' => 'media' を指定しておくと、新規投稿時に自動で設定されます。
function k_suke_works_template() {
$post_type_object = get_post_type_object( 'works' );
if ( $post_type_object ) {
$post_type_object->template = array(
array( 'core/gallery', array( 'linkTo' => 'media' ) ),
);
}
}
add_action( 'init', 'k_suke_works_template' );
一覧に写真が全部並んでしまうのはどう防ぐ?
CSSで2枚目以降を隠します。
クエリループはギャラリーブロックの中身をそのまま出力するので、登録した写真が全部並びます。1件につき10枚あれば、10枚とも表示されてしまいます。
/* ギャラリーの2枚目以降を隠す */
.works-card-list .wp-block-gallery .wp-block-image:not(:first-child) {
display: none;
}
:not(:first-child) は「1つ目以外」という指定です。これ1行で、一覧はカード型に、Lightboxでは全枚数が見られるという状態が作れます。隠しているだけで、HTMLには存在しているためです。
カード全体の見た目はこちらです。
/* カードの枠組み */
.works-card-list .wp-block-post {
border: 1px solid #d4d4d4;
padding: 5px;
background-color: #fff;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
margin-bottom: 24px;
}
/* ホバーで浮き上がる */
.works-card-list .wp-block-post:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
border-color: #bbb;
}
/* 画像まわり */
.works-card-list .wp-block-post-content {
margin: 0;
padding: 0;
}
.works-card-list .wp-block-gallery {
margin-bottom: 10px !important;
}
.works-card-list .wp-block-gallery .wp-block-image img {
cursor: pointer;
transition: opacity 0.3s;
}
.works-card-list .wp-block-gallery .wp-block-image img:hover {
opacity: 0.8;
}
/* タイトル帯 */
.works-card-list .works-card-ttl {
background-color: #386ab1;
padding: 5px 5px 5px 10px;
margin-top: 5px;
}
/* 項目行(点線区切り・最終行は線なし) */
.works-card-list .works-card-item-column {
padding-top: 7px !important;
padding-bottom: 7px !important;
font-size: 0.87em;
border-bottom: 1px dashed #b5b5b5;
}
.works-card-list .works-card-item-column:last-child {
border-bottom: none;
}
.wp-block-post は、クエリループが各投稿を包むときに自動で付けるクラスです。ここに枠と影を当てるとカードになります。
クエリループブロックの「高度な設定 → 追加CSSクラス」に works-card-list を入れるのを忘れないでください。CSSが効かない原因の大半がこれです。
つまずきやすい5つのポイント
実装するとき、この5つで詰まります。
| 症状 | 原因 |
|---|---|
| カスタムフィールド欄が見つからない | 投稿編集画面の「⋮」→ 設定 → パネル でONにする |
| CSSがまったく効かない | クエリループに works-card-list を付け忘れ |
| フィールドの値が出ない | 段落ブロックに display-works-name などのクラスを付け忘れ |
| Lightboxが開かない | ギャラリー画像のリンク先が「メディアファイル」でない |
| 関係ないページの段落が消えた | フラグ判定が抜けている。is_works_loop_active の確認を |
この方法は誰に向いているのか?
ページビルダーの制約下で、プラグインを増やしたくない人です。
向いているケース
- ページビルダーを使っていて、クエリループを置けない
- ACFを入れずにカスタムフィールドを表示したい
- 表示項目が数個で、レイアウトを自分で決めたい
向いていないケース
- 表示項目が10個以上ある。管理しきれなくなります
- クライアントが自分でレイアウトも変更したい。パターンとPHPの両方を触ることになります
- ブロック開発の知識があり、自作ブロックを作れる。そちらのほうが正攻法です
正直なところ、この実装は正攻法ではありません。preg_replace でHTMLを書き換えているので、WordPress側の出力仕様が変われば動かなくなる可能性があります。
それでも、ページビルダーの制約下でプラグインを増やさずに実現するという条件では、現実的な選択肢だと考えています。
まとめ
| 課題 | 解決方法 |
|---|---|
| ビルダーにクエリループを置けない | パターン化して do_blocks() でショートコード出力 |
| クエリループがカスタムフィールドを出せない | render_block で段落の中身を差し替え |
| フィルターがサイト全体に効く | ショートコード実行中だけグローバルフラグを立てる |
| Lightboxが全画像でつながる | data-rel に投稿IDを埋めてグループ分け |
| 一覧に写真が全部並ぶ | :not(:first-child) で2枚目以降を非表示 |
3回の方針転換を経ていますが、振り返るとどれも必要な回り道でした。特に投稿タイプ判定が効かなかった経験は、render_block フィルターの性質を理解するうえで大きかったです。
なお、この実装には子テーマが必須です。functions.php を編集するので、親テーマに直接書くとテーマ更新で消えます。


💬 まめすけ うまくいかなかった回り道こそ、次の誰かの近道になるんだね



うまくいかなかった回り道こそ、次の誰かの近道になるんだね









コメント