WordPressで施工事例やポートフォリオを作成する際、複数の画像を登録・並び替えできる「繰り返しフィールド」が欲しくなることはありませんか?
Advanced Custom Fields(ACF)Proを使えば簡単に実装できますが、年間ライセンス費用がかかるのがネックです。
しかし、実は、画像ギャラリーの繰り返しフィールドはWordPressの標準機能だけで自作できます。
この記事では、ACF Proを使わずにカスタムメタボックスで画像ギャラリーを実装する方法を、実際のコード付きで解説します。
この記事でできること
- カスタム投稿タイプに画像ギャラリー機能を追加
- WordPress標準のメディアライブラリから複数画像を選択
- ドラッグ&ドロップで画像の並び替え
- 個別の画像削除
- ACF Proのライセンス費用を節約
完成イメージ
管理画面の投稿編集ページに、以下の機能を持つメタボックスが追加されます。
- 「画像を追加」ボタンでメディアライブラリが開く
- サムネイル一覧が表示され、ドラッグで並び替え可能
- 各画像の右上に削除ボタン(×)を配置

前提条件
- カスタム投稿タイプ
works(施工事例)を今記事の例とする - カスタム投稿タイプ
works(施工事例)が登録済みであること - WordPress 5.0以上であること
カスタム投稿タイプの登録がまだの場合は、functions.php に register_post_type() で works を登録してください。
実装の全体像
実装は大きく4つのパートに分かれます。
- 管理画面用スクリプトの読み込み — メディアライブラリとSortableを有効化
- メタボックスの追加 — 投稿編集画面にギャラリーUIを表示
- メタボックスのHTML/JS — 画像選択・削除・並び替えのUI実装
- 保存処理 — 画像IDの配列をポストメタに保存
コード解説
以下のコードを functions.php に追加してください。
1. 管理画面用スクリプトの読み込み
function works_gallery_admin_scripts($hook) {
global $post_type;
if (($hook === 'post.php' || $hook === 'post-new.php') && $post_type === 'works') {
wp_enqueue_media();
wp_enqueue_script('jquery-ui-sortable');
}
}
add_action('admin_enqueue_scripts', 'works_gallery_admin_scripts');
wp_enqueue_media() でWordPress標準のメディアアップローダーを、jquery-ui-sortable でドラッグ&ドロップの並び替え機能を読み込みます。
$hook と $post_type で条件を絞ることで、施工事例の編集画面でのみスクリプトをロードし、他の管理画面への影響を防いでいます。
2. メタボックスの登録
function works_gallery_meta_box() {
add_meta_box(
'works_gallery', // メタボックスのID
'施工事例 画像ギャラリー', // 表示タイトル
'works_gallery_meta_box_callback', // コールバック関数
'works', // 対象の投稿タイプ
'normal', // 表示位置
'high' // 優先度
);
}
add_action('add_meta_boxes', 'works_gallery_meta_box');
add_meta_box() で投稿編集画面にカスタムメタボックスを追加します。
第5引数の 'normal' はエディタの下に表示する指定、'high' は他のメタボックスより上に表示する優先度の指定です。
3. メタボックスのHTML出力とJavaScript
ここがメインの実装部分です。
少し長いですが、やっていることはシンプルです。
// メタボックスのHTML出力
function works_gallery_meta_box_callback($post) {
wp_nonce_field('works_gallery_nonce_action', 'works_gallery_nonce');
$images = get_post_meta($post->ID, '_works_gallery_images', true);
if (!is_array($images)) $images = array();
?>
<style>
#works-gallery-container {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.works-gallery-item {
cursor: grab;
position: relative;
display: inline-block;
}
.works-gallery-item:active {
cursor: grabbing;
}
.works-gallery-item.ui-sortable-helper {
opacity: 0.8;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
transform: scale(1.05);
}
.sortable-placeholder {
width: 150px;
height: 150px;
border: 2px dashed #0073aa;
background: #f0f6fc;
display: inline-block;
margin: 5px;
}
</style>
<div id="works-gallery-container">
<?php foreach ($images as $index => $image_id) :
$image_url = wp_get_attachment_image_url($image_id, 'thumbnail');
if (!$image_url) continue;
?>
<div class="works-gallery-item">
<img src="<?php echo esc_url($image_url); ?>" style="width:150px; height:150px; object-fit:cover; display:block;">
<input type="hidden" name="works_gallery_images[]" value="<?php echo esc_attr($image_id); ?>">
<button type="button" class="works-gallery-remove" style="position:absolute; top:0; right:0; background:red; color:#fff; border:none; cursor:pointer; padding:2px 6px;">×</button>
</div>
<?php endforeach; ?>
</div>
<p>
<button type="button" id="works-gallery-add" class="button">画像を追加</button>
</p>
<p class="description">※ 画像をドラッグ&ドロップで並び替えできます</p>
<script>
jQuery(document).ready(function($) {
$('#works-gallery-add').on('click', function(e) {
e.preventDefault();
var frame = wp.media({
title: '画像を選択',
multiple: true,
library: { type: 'image' },
button: { text: '画像を追加' }
});
frame.on('select', function() {
var attachments = frame.state().get('selection').toJSON();
$.each(attachments, function(i, attachment) {
var thumbUrl = attachment.sizes.thumbnail ? attachment.sizes.thumbnail.url : attachment.url;
var html = '<div class="works-gallery-item">';
html += '<img src="' + thumbUrl + '" style="width:150px; height:150px; object-fit:cover; display:block;">';
html += '<input type="hidden" name="works_gallery_images[]" value="' + attachment.id + '">';
html += '<button type="button" class="works-gallery-remove" style="position:absolute; top:0; right:0; background:red; color:#fff; border:none; cursor:pointer; padding:2px 6px;">×</button>';
html += '</div>';
$('#works-gallery-container').append(html);
});
});
frame.open();
});
$(document).on('click', '.works-gallery-remove', function() {
$(this).closest('.works-gallery-item').remove();
});
$('#works-gallery-container').sortable({
items: '.works-gallery-item',
cursor: 'grabbing',
placeholder: 'sortable-placeholder',
tolerance: 'pointer',
revert: 200
});
});
</script>
<?php
}
ポイント解説
nonceフィールド:wp_nonce_field() でCSRF対策を実施しています。保存時にこのnonceを検証することで、不正なリクエストを防ぎます。
hidden input:各画像の attachment ID を works_gallery_images[] という配列名のhiddenフィールドで保持します。Sortableで並び替えるとDOM順が変わるため、フォーム送信時に自動的に正しい順序で送信されます。
wp.media:WordPressに組み込まれているメディアライブラリのモーダルを利用しています。multiple: true で複数選択に対応しています。
jQuery UI Sortable:.sortable() を呼ぶだけでドラッグ&ドロップの並び替えが実現します。placeholder オプションで、ドロップ先に青い点線の枠を表示しています。
4. 保存処理
function works_gallery_save($post_id) {
// nonceの検証
if (!isset($_POST['works_gallery_nonce']) ||
!wp_verify_nonce($_POST['works_gallery_nonce'], 'works_gallery_nonce_action')) return;
// 自動保存時はスキップ
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
// 権限チェック
if (!current_user_can('edit_post', $post_id)) return;
// 画像IDの保存
if (isset($_POST['works_gallery_images'])) {
$images = array_map('intval', $_POST['works_gallery_images']);
update_post_meta($post_id, '_works_gallery_images', $images);
} else {
delete_post_meta($post_id, '_works_gallery_images');
}
}
add_action('save_post_works', 'works_gallery_save');
保存処理では3つのセキュリティチェックを行っています。
- nonce検証:リクエストが正規のフォームからであることを確認
- 自動保存の除外:WordPressの自動保存でメタデータが消えるのを防止
- 権限チェック:投稿を編集する権限を持つユーザーかを確認
array_map('intval', ...) で送信された値をすべて整数に変換し、不正な値の混入を防いでいます。
save_post_works フックは save_post_{post_type} 形式で、施工事例(works)の保存時にのみ発火します。
フロントエンドでの表示方法
保存した画像をフロントエンドで表示するには、テンプレートファイルに以下のコードを追加します。
<?php
$images = get_post_meta(get_the_ID(), '_works_gallery_images', true);
if (!empty($images) && is_array($images)) : ?>
<div class="works-gallery">
<?php foreach ($images as $image_id) :
$image_url = wp_get_attachment_image_url($image_id, 'large');
$image_alt = get_post_meta($image_id, '_wp_attachment_image_alt', true);
if (!$image_url) continue;
?>
<figure class="works-gallery__item">
<img src="<?php echo esc_url($image_url); ?>"
alt="<?php echo esc_attr($image_alt); ?>"
loading="lazy">
</figure>
<?php endforeach; ?>
</div>
<?php endif; ?>
wp_get_attachment_image_url() の第2引数でサイズを指定できます。
一覧ページでは medium、詳細ページでは large や full を使い分けるとよいでしょう。
まとめ
ACF Proの繰り返しフィールドは便利ですが、画像ギャラリー程度の機能であればWordPressの標準機能だけで十分に実装できます。
今回紹介したコードは約100行程度で、メディアライブラリの活用、ドラッグ&ドロップの並び替え、セキュリティ対策まで含んだ実用的な実装です。プラグインへの依存を減らしたい方や、WordPressの内部構造を学びたい方はぜひ試してみてください。
