Hibiiroの制作物紹介を作るとき、最初に考えたのはページの見た目でした。
ただ、アプリが増えるたびにPHPへタイトルやリンクを書き足す構成では、更新するたびにテーマのファイルを差し替えることになります。
そこで、制作物の情報を固定ページに保存し、子テーマはその情報を読み込んで表示する構成にしました。
今回は、固定ページと投稿メタを使って、制作物一覧を管理画面から更新できるようにした仕組みを紹介します。
固定ページの標準項目と、独自の項目を分ける
作品ごとに固定ページを作り、WordPressの標準項目をそのまま使えるものは使います。
| 情報 | 保存する場所 |
|---|---|
| 制作物の名前 | 固定ページのタイトル |
| 詳しい説明 | 固定ページの本文 |
| 詳細ページのURL | 固定ページのパーマリンク |
| 一覧用の紹介文 | 投稿メタ |
| 種類・並び順・ホーム掲載設定 | 投稿メタ |
| アイコン・紹介画像・公開先リンク | 投稿メタ |
名前やURLを別の設定画面にも保存すると、片方だけ更新して食い違う可能性があります。
そのため、詳細ページの名前とURLは、表示するときにWordPressから取得します。
$d['id'] = $page->ID;
$d['name'] = get_the_title($page->ID);
$d['detail'] = get_permalink($page->ID);タイトルやパーマリンクを変更しても、一覧側で同じ情報を入力し直す必要がありません。
制作物の情報は、投稿メタの配列として保存する
独自の情報は、_hibiiro_work_data というキーにまとめています。
保存するデータの一部を簡略化すると、次のようになります。
$data = [
'enabled' => 1,
'hidden' => 0,
'home' => 1,
'order' => 20,
'type' => 'app',
'intro' => '一覧に表示する短い紹介文',
'links' => [
[
'label' => '公開先を見る',
'url' => 'https://example.com/app/',
],
],
];enabled は制作物として扱うか、hidden は一覧から隠すか、home はホームに掲載するかを表します。
この三つを分けると、例えば「詳細ページは公開したまま、ホームには載せない」という使い方ができます。
なお、一覧から隠すことはアクセス制限ではありません。ページを見せたくない場合は、固定ページ自体の公開状態で管理します。
保存処理では、入力値より先に権限と送信元を確認する
入力欄を作るだけでなく、保存処理も必要です。
Hibiiroでは、固定ページの保存時に save_post_page で処理しています。
function hibiiro_work_save($id) {
if (
get_post_type($id) !== 'page' ||
wp_is_post_revision($id) ||
wp_is_post_autosave($id)
) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
$nonce = $_POST['hibiiro_work_nonce'] ?? '';
if (
!is_string($nonce) ||
!wp_verify_nonce($nonce, 'hibiiro_work_save') ||
!current_user_can('edit_post', $id)
) {
return;
}
if (
!isset($_POST['hibiiro_work']) ||
!is_array($_POST['hibiiro_work'])
) {
return;
}
$data = hibiiro_work_sanitize(
wp_unslash($_POST['hibiiro_work'])
);
update_post_meta(
$id,
'_hibiiro_work_data',
wp_slash($data)
);
update_post_meta(
$id,
'_hibiiro_is_work',
(string) $data['enabled']
);
}
add_action('save_post_page', 'hibiiro_work_save');管理画面側には、この処理に対応するnonceの入力欄を置いています。
この保存処理では、自動保存やリビジョンを除外し、nonceと編集権限を確認してからデータを保存します。独自入力欄のデータが送信されていなければ処理を終了するため、関係のない保存操作で値を空にしてしまうことも避けられます。
hibiiro_work_sanitize() は独自の関数です。文字列・URL・チェック項目などを、用途ごとに整えています。
URLと文章では、入力の扱いを変える
例えば、紹介文とURLは同じ方法では処理していません。
$intro = is_string($raw['intro'] ?? null)
? $raw['intro']
: '';
$data['intro'] = sanitize_textarea_field($intro);
$url = is_string($raw['url'] ?? null)
? $raw['url']
: '';
$data['url'] = esc_url_raw($url, ['http', 'https']);紹介文は改行を使えるテキストとして扱い、URLはHTTP・HTTPSに限定しています。
選択式の種類についても、送られてきた値をそのまま信用せず、登録済みの選択肢にあるかを確認します。
保存前に入力を整理することと、表示するときにHTMLとして安全な形へ変換することは、別々に行っています。
wp_unslash() と wp_slash() を両方使う理由
保存処理では、一度 wp_unslash() したあと、保存直前に wp_slash() を使っています。
少し回りくどく見えますが、役割が違います。
wp_unslash():WordPressによってスラッシュ処理された入力を戻してから検証するwp_slash():update_post_meta()の保存時の扱いに合わせる
update_post_meta() では、保存時にスラッシュが除去されます。バックスラッシュを含む文章などをそのまま渡すと、意図した文字列を維持できない場合があるため、保存前に対応しています。
一覧には、公開済みでパスワードのないページだけを取得する
一覧の取得条件は、次のようにしています。
$pages = get_posts([
'post_type' => 'page',
'post_status' => 'publish',
'has_password' => false,
'numberposts' => -1,
'meta_query' => [
'relation' => 'OR',
[
'key' => '_wp_page_template',
'value' => hibiiro_work_templates(),
'compare' => 'IN',
],
[
'key' => '_hibiiro_is_work',
'value' => '1',
],
],
'orderby' => 'ID',
'order' => 'ASC',
]);hibiiro_work_templates() は、制作物用テンプレートのファイル名を返す独自関数です。
専用テンプレートを使うページと、制作物として設定されたページを取得します。公開状態とパスワードの条件を先に指定しているため、下書きやパスワード付きページを制作物一覧へ混ぜない構成です。
_hibiiro_is_work を別のメタデータとして保存しているのは、検索に使うためです。
詳細データの配列はまとめて保存しつつ、「一覧へ取得する対象か」という検索条件だけは単独の値として持たせています。保存された配列の中身を文字列検索する方法にはしていません。
取得したあと、掲載先の条件と並び順を適用する
取得したページを、ホーム用・アプリ一覧用などに分けます。
if (!$data['enabled'] || $data['hidden']) {
continue;
}
if ($where === 'home' && !$data['home']) {
continue;
}
if (
$where === 'apps' &&
!in_array($data['type'], ['app', 'game'], true)
) {
continue;
}
$items[] = $data;同じ制作物データから、表示する場所によって対象を変えています。
並び順は設定値を優先し、同じ値なら投稿IDで決めます。
usort($items, function ($a, $b) {
return ($a['order'] <=> $b['order'])
?: ($a['id'] <=> $b['id']);
});二つ目の条件を置くことで、同じ並び順の作品が複数あっても、順序を一定にできます。
表示側では、文章・URL・属性を分けて出力する
カードを描画するときは、保存データをそのままHTMLに流し込まず、出力先に合わせてエスケープします。
<a href="<?php echo esc_url($item['detail']); ?>">
<?php echo esc_html($item['name']); ?>
</a>
<p><?php echo esc_html($item['intro']); ?></p>画像の代替テキストなど、属性の中へ入れる値には esc_attr() を使います。
<img
src="<?php echo esc_url($item['image']); ?>"
alt="<?php echo esc_attr($item['image_alt']); ?>"
loading="lazy"
>これにより、紹介文に記号が含まれていても、意図せずHTMLとして解釈されることを防ぎます。
この構成にも、件数が増えたときの限界はある
現在の実装では、対象となる固定ページを numberposts => -1 でまとめて取得し、PHP側で絞り込みと並び替えをしています。
制作物を少数ずつ管理するには扱いやすい構成ですが、数百・数千件へ増える場合は見直しが必要です。
その段階では、掲載先や並び順も検索しやすい独立した項目にし、データベース側で絞り込み・並び替え・ページ分割する方法を検討できます。
まずは今の規模に合う構成にし、件数が増えたら取得方法を変えられるよう、取得と表示の処理を分けています。
公開状態と「空にする操作」を確認する
このような入力欄では、値を追加できることだけでなく、消せることも確認したいところです。
- 下書きの制作物が一覧に出ない
- パスワード付きページが一覧に出ない
- ホーム掲載を外すと、ホームだけから消える
- 紹介文やリンクを空にして保存できる
- 同じ並び順でも表示順が安定する
- タイトルを変えると一覧の名前も変わる
特にリンク欄では、「入力欄が送信されなかった」と「利用者がすべて削除した」を区別する必要があります。Hibiiroでは、リンク欄の送信を示すマーカーを設け、全削除した場合も保存できるようにしています。
デザインの変更と、制作物の追加を分ける
この構成にしても、テンプレートやレイアウトを変えるときにはテーマの更新が必要です。
一方で、制作物の追加、紹介文の修正、リンクの追加、並び順の変更は管理画面で行えます。
今回変えたかったのは、その境目です。見た目や仕組みは子テーマで管理し、日々増える情報はWordPressへ保存する。そうすることで、制作物を増やすたびにPHPを編集する必要がない構成にしました。

コメント