テクニック

WordPressカスタム投稿の一覧テンプレートの作り方|実装手順とコード

この記事の対象: 実績・お知らせをカスタム投稿タイプで整理したいWordPress制作者
読了時間: 約12分

実績、お知らせ、導入事例。通常の投稿(post)にカテゴリーを足して運用しはじめると、しばらく経ってから「お知らせのつもりで書いた記事が実績一覧に混ざる」という事故が起きます。カスタム投稿タイプで箱そのものを分けておけば、そもそも混ざりません。この記事では投稿タイプの登録からタクソノミー、一覧テンプレートの実装、パーマリンクの設計までを、コピーしてそのまま動くコードで通します。あとから変更すると何が壊れるのかも先に書いておきます。


前提と、この記事で作るもの

動作環境

項目 前提
WordPress 6.0 以降で確認。register_post_type 自体は 3.0 から使えます
テーマ クラシックテーマ(PHPテンプレート階層を使うもの)。ブロックテーマの場合は後述の節を読んでください
PHP 7.4 以降
権限 管理者。テーマファイルの編集ができること
プラグイン 不要。素のWordPressだけで完結します

コードの置き場所は2通りあります。

  • テーマの functions.php — テーマと一体で運用する場合
  • mu-plugins または自作プラグイン — テーマを入れ替えてもデータを残したい場合

投稿タイプの登録は「コンテンツの箱の定義」なので、本来はテーマではなくプラグイン側の仕事です。ただ、実務では専用テーマとセットで納品することが多く、その場合は functions.php でも問題ありません。テーマ変更の予定があるなら wp-content/mu-plugins/cpt.php に置いてください(mu-pluginsは有効化操作なしで自動的に読み込まれます)。

この記事では functions.php に書く前提で進めます。ファイルパスは wp-content/themes/お使いのテーマ/functions.php です。有料テーマを使っている場合は、必ず子テーマの functions.php に書いてください。親テーマを直接編集するとアップデートで消えます。

完成する構造

この記事で作るカスタム投稿タイプの構造

works(実績)

  • work_cat(業種)
  • work_tag(対応内容)

news(お知らせ)

  • news_cat(種別)

post(通常の投稿・ブログ用に残す)

実績とお知らせで分類軸が違うので、タクソノミーは共有せず個別に持たせます。

実績は「業種」と「対応内容」の2軸で絞りたくなりますが、お知らせにその軸は要りません。共通のタクソノミーを1つ作って両方に紐づける設計もできますが、管理画面のドロップダウンに関係ない項目が並ぶので、更新する人が迷います。分けたほうが親切です。


カスタム投稿タイプを登録する

最小構成のコード

functions.php の末尾に追記します。

<?php
/**
 * カスタム投稿タイプ「実績(works)」を登録する
 */
function my_register_works_post_type() {
    $labels = array(
        'name'               => '実績',
        'singular_name'      => '実績',
        'menu_name'          => '実績',
        'add_new'            => '新規追加',
        'add_new_item'       => '実績を新規追加',
        'edit_item'          => '実績を編集',
        'new_item'           => '新しい実績',
        'view_item'          => '実績を表示',
        'search_items'       => '実績を検索',
        'not_found'          => '実績が見つかりませんでした',
        'not_found_in_trash' => 'ゴミ箱に実績はありません',
        'all_items'          => '実績一覧',
    );

    $args = array(
        'labels'        => $labels,
        'public'        => true,
        'has_archive'   => true,
        'menu_position' => 5,
        'menu_icon'     => 'dashicons-portfolio',
        'supports'      => array( 'title', 'editor', 'thumbnail', 'excerpt', 'revisions' ),
        'rewrite'       => array( 'slug' => 'works', 'with_front' => false ),
        'show_in_rest'  => true,
    );

    register_post_type( 'works', $args );
}
add_action( 'init', 'my_register_works_post_type' );

変更する箇所は次のとおりです。

箇所 意味 変更の目安
works(第1引数) 投稿タイプ名。DBに保存される識別子 20文字以内・英小文字とアンダースコアのみ。あとから変えると全記事が消えたように見える
$labels の各値 管理画面に出る文言 クライアントが使う言葉に合わせる(「実績」か「制作事例」か)
menu_position 管理メニューの並び順 5=投稿の直下、20=固定ページの下
menu_icon メニューアイコン Dashicons のクラス名。dashicons-megaphone dashicons-building など
supports 編集画面に出す機能 アイキャッチが要らないなら thumbnail を外す
rewrite.slug URLに出る文字列 /works/記事スラッグ/ になる

has_archive と show_in_rest の意味

この2つが、あとで一覧を作れるかどうかを決めます。

  • has_archive => true — /works/ にアクセスしたとき、一覧ページ(アーカイブ)を出す。false だと /works/ は404になり、archive-works.php を置いても表示されません。一覧ページを作るなら必須です。
  • show_in_rest => true — ブロックエディター(Gutenberg)を有効にする。false にすると旧エディターになります。WordPress 5.0 以降でブロックエディターを使いたいなら true。

has_archive に文字列を渡すこともできます。'has_archive' => 'case-study' とすると、個別ページは /works/xxx/、一覧は /case-study/ という分離もできますが、混乱するので特別な理由がなければ true のままにしてください。

with_front は false にしておく

rewrite の with_front は、パーマリンク設定の先頭部分を引き継ぐかどうかの指定です。

パーマリンク設定が /blog/%postname%/ になっている環境で with_front => true(既定値)のままだと、実績のURLが /blog/works/xxx/ になります。実績はブログの下ではないので、false を明示しておくのが安全です。パーマリンク設定が /%postname%/ の場合は結果が変わらないので気づきませんが、あとで設定を変えた瞬間にURLが動きます。最初から書いておくべき一行です。

登録したら必ずやること

コードを保存しただけでは、一覧ページが404になります。

管理画面 → 設定 → パーマリンク → 「変更を保存」を押す(何も変更しなくて構いません)。これでリライトルールが再生成されます。

コードで自動化することもできますが、flush_rewrite_rules() を init で毎回呼ぶのは重いのでやめてください。テーマ有効化時に一度だけ流す形が正解です。

/**
 * テーマ有効化時に一度だけリライトルールを再生成する
 */
function my_flush_rewrite_on_activation() {
    my_register_works_post_type();
    flush_rewrite_rules();
}
add_action( 'after_switch_theme', 'my_flush_rewrite_on_activation' );

タクソノミー(分類)を追加する

一覧に絞り込みを付けるなら、カテゴリーに相当するタクソノミーが必要です。同じく functions.php に追記します。

/**
 * 実績用のタクソノミー「業種(work_cat)」を登録する
 */
function my_register_works_taxonomy() {
    // 階層あり(カテゴリー型)
    register_taxonomy(
        'work_cat',
        'works',
        array(
            'labels'            => array(
                'name'          => '業種',
                'singular_name' => '業種',
                'add_new_item'  => '業種を追加',
                'edit_item'     => '業種を編集',
                'all_items'     => '業種一覧',
            ),
            'public'            => true,
            'hierarchical'      => true,
            'show_admin_column' => true,
            'show_in_rest'      => true,
            'rewrite'           => array( 'slug' => 'works/category', 'with_front' => false ),
        )
    );

    // 階層なし(タグ型)
    register_taxonomy(
        'work_tag',
        'works',
        array(
            'labels'            => array(
                'name'          => '対応内容',
                'singular_name' => '対応内容',
                'add_new_item'  => '対応内容を追加',
                'all_items'     => '対応内容一覧',
            ),
            'public'            => true,
            'hierarchical'      => false,
            'show_admin_column' => true,
            'show_in_rest'      => true,
            'rewrite'           => array( 'slug' => 'works/tag', 'with_front' => false ),
        )
    );
}
add_action( 'init', 'my_register_works_taxonomy' );

ここでも保存後にパーマリンク設定を開き直します。

hierarchical の値で管理画面のUIが変わります。

hierarchical の値で編集画面の見た目が変わる

true(カテゴリー型)

  • チェックボックスで選ぶ
  • 親子関係を持てる
  • 選択肢を固定したいときに向く

false(タグ型)

  • 自由入力で追加できる
  • 親子関係を持てない
  • 語が増え続けて散らかりやすい

クライアントが更新する項目は hierarchical => true にしてください。自由入力にすると「製造業」「製造」「メーカー」が並立して、絞り込みが機能しなくなります。

show_admin_column => true は地味ですが効きます。管理画面の一覧に業種の列が出るので、件数が増えたときに探す手間が変わります。

タクソノミーのスラッグを works/category にした理由

'slug' => 'work_cat' にすると、URLが /work_cat/manufacturing/ という形になります。これはサイト構造として不自然です。works/category にしておけば /works/category/manufacturing/ となり、パンくずリストの構造とURLが一致します。

ただし注意点があります。/works/ 配下にスラッグを置くと、投稿タイプの個別ページのURLと衝突する可能性があります。category という名前の実績記事を作ると、/works/category/ がどちらを指すか曖昧になります。実務ではまず起きませんが、タクソノミーのスラッグに使った語(category tag)を記事スラッグに使わないよう、社内ルールとして共有しておくと安全です。


一覧テンプレートを作る

テンプレート階層のルール

WordPressはファイル名で自動的にテンプレートを選びます。カスタム投稿タイプ works の場合、次の順で探して最初に見つかったものを使います。

URL 探すファイル(上から順に)
/works/ archive-works.php → archive.php → index.php
/works/記事スラッグ/ single-works.php → single.php → index.php
/works/category/xxx/ taxonomy-work_cat.php → taxonomy.php → archive.php → index.php

つまり archive-works.php を置くだけで、/works/ の見た目を専用にできます。プラグインもフックも不要です。

archive-works.php(そのまま動きます)

wp-content/themes/お使いのテーマ/archive-works.php として新規作成します。

<?php
/**
 * 実績(works)の一覧テンプレート
 */
get_header();
?>

<main class="works-archive">
    <header class="works-archive__head">
        <h1 class="works-archive__title"><?php post_type_archive_title(); ?></h1>
    </header>

    <?php
    // 業種による絞り込みリンク
    $terms = get_terms(
        array(
            'taxonomy'   => 'work_cat',
            'hide_empty' => true,
        )
    );
    if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) :
        ?>
        <nav class="works-filter" aria-label="業種で絞り込む">
            <ul class="works-filter__list">
                <li class="works-filter__item">
                    <a class="works-filter__link is-current" href="<?php echo esc_url( get_post_type_archive_link( 'works' ) ); ?>">すべて</a>
                </li>
                <?php foreach ( $terms as $term ) : ?>
                    <li class="works-filter__item">
                        <a class="works-filter__link" href="<?php echo esc_url( get_term_link( $term ) ); ?>">
                            <?php echo esc_html( $term->name ); ?>
                        </a>
                    </li>
                <?php endforeach; ?>
            </ul>
        </nav>
    <?php endif; ?>

    <?php if ( have_posts() ) : ?>
        <ul class="works-list">
            <?php
            while ( have_posts() ) :
                the_post();
                ?>
                <li class="works-list__item">
                    <a class="works-card" href="<?php the_permalink(); ?>">
                        <div class="works-card__thumb">
                            <?php if ( has_post_thumbnail() ) : ?>
                                <?php the_post_thumbnail( 'medium_large', array( 'loading' => 'lazy' ) ); ?>
                            <?php else : ?>
                                <span class="works-card__noimage" aria-hidden="true"></span>
                            <?php endif; ?>
                        </div>
                        <div class="works-card__body">
                            <?php
                            $post_terms = get_the_terms( get_the_ID(), 'work_cat' );
                            if ( $post_terms && ! is_wp_error( $post_terms ) ) :
                                ?>
                                <span class="works-card__cat"><?php echo esc_html( $post_terms[0]->name ); ?></span>
                            <?php endif; ?>
                            <h2 class="works-card__title"><?php the_title(); ?></h2>
                            <p class="works-card__excerpt"><?php echo esc_html( wp_trim_words( get_the_excerpt(), 60, '…' ) ); ?></p>
                        </div>
                    </a>
                </li>
            <?php endwhile; ?>
        </ul>

        <nav class="works-pager" aria-label="ページ送り">
            <?php
            the_posts_pagination(
                array(
                    'mid_size'  => 1,
                    'prev_text' => '前へ',
                    'next_text' => '次へ',
                )
            );
            ?>
        </nav>

    <?php else : ?>
        <p class="works-empty">まだ実績が登録されていません。</p>
    <?php endif; ?>
</main>

<?php
get_footer();

変更する箇所:

  • works- から始まるクラス名 — 既存テーマのCSS設計に合わせて置き換えてください
  • wp_trim_words( ..., 60, '…' ) の 60 — 抜粋の文字数。日本語では単語数ではなく文字数として働くことが多いので、実際の表示を見て調整します
  • the_post_thumbnail( 'medium_large', ... ) — 画像サイズ。カードが小さいなら medium、大きく見せるなら large
  • 「まだ実績が登録されていません。」— 納品先に合わせた文言に

get_the_terms() は該当タームがないと false、投稿タイプが存在しないと WP_Error を返します。is_wp_error() のチェックを外すと、条件次第で PHP のエラーが出ます。省略しないでください。

一覧に出す情報を絞る

一覧カードに載せる要素は、タイトル・アイキャッチ・業種・抜粋の4つで足ります。実務でよく足したくなるのが「公開日」ですが、実績一覧では逆効果になることがあります。最新の実績が2年前だと分かってしまうためです。日付を出すかどうかは、更新頻度が担保できるかで決めてください。お知らせなら日付は必須、実績なら任意、という切り分けが扱いやすいです。

日付を出す場合は、カードのタイトル手前にこの1行を足します。

<time class="works-card__date" datetime="<?php echo esc_attr( get_the_date( DATE_W3C ) ); ?>"><?php echo esc_html( get_the_date( 'Y.m.d' ) ); ?></time>

datetime 属性には機械可読の形式(DATE_W3C)を、画面には日本語サイトで読みやすい Y.m.d を入れています。両方を get_the_date( 'Y.m.d' ) で済ませると、機械側が日付として解釈できません。

表示件数を投稿タイプごとに変える

一覧の件数は「設定 → 表示設定」の値が全体に適用されます。実績だけ12件、お知らせは20件にしたい場合は pre_get_posts で分けます。

/**
 * 一覧の表示件数を投稿タイプごとに変える
 */
function my_set_posts_per_page( $query ) {
    // 管理画面とサブクエリには効かせない
    if ( is_admin() || ! $query->is_main_query() ) {
        return;
    }

    if ( $query->is_post_type_archive( 'works' ) || $query->is_tax( array( 'work_cat', 'work_tag' ) ) ) {
        $query->set( 'posts_per_page', 12 );
    }

    if ( $query->is_post_type_archive( 'news' ) ) {
        $query->set( 'posts_per_page', 20 );
    }
}
add_action( 'pre_get_posts', 'my_set_posts_per_page' );

is_admin() と is_main_query() の2行を必ず入れてください。 これを省くと管理画面の投稿一覧まで12件に変わり、「一覧に出てこない」というクレームが来ます。ここは事故が起きやすい場所です。

12 の部分が変更箇所です。カードが3列なら3の倍数、4列なら4の倍数にすると最終行が欠けません。

並び順を制御する

実績一覧は公開日順ではなく、見せたい順に並べたいことがあります。その場合は supports に page-attributes を足して「順序」フィールドを出し、pre_get_posts で並び替えます。

// register_post_type の supports に 'page-attributes' を追加した上で
if ( $query->is_post_type_archive( 'works' ) ) {
    $query->set( 'orderby', array( 'menu_order' => 'ASC', 'date' => 'DESC' ) );
}

menu_order は既定値が全記事 0 なので、そのままだと日付順のまま動きます。上に出したい記事だけ -1 を入れる運用にすると、クライアント側の操作が「上げたいものに -1」の1つで済みます。数字を大きい順に振り直す運用は、記事が増えたときに破綻します。

タクソノミーアーカイブのテンプレート

/works/category/manufacturing/ を専用の見た目にするなら taxonomy-work_cat.php を作ります。ただし archive-works.php とほとんど同じ内容になるので、実務では共通化します。

taxonomy-work_cat.php:

<?php
/**
 * 業種(work_cat)のアーカイブ。一覧の中身は archive-works.php と共通
 */
get_header();
?>

<main class="works-archive">
    <header class="works-archive__head">
        <h1 class="works-archive__title"><?php single_term_title(); ?>の実績</h1>
        <?php
        $description = term_description();
        if ( $description ) {
            echo '<div class="works-archive__desc">' . wp_kses_post( $description ) . '</div>';
        }
        ?>
    </header>

    <?php get_template_part( 'template-parts/works', 'loop' ); ?>
</main>

<?php
get_footer();

wp-content/themes/お使いのテーマ/template-parts/works-loop.php に、先ほどの archive-works.php のループ部分(if ( have_posts() ) から endif; まで)を切り出して置きます。archive-works.php 側も get_template_part( 'template-parts/works', 'loop' ); に差し替えれば、修正箇所が1つになります。

get_template_part( 'template-parts/works', 'loop' ) は template-parts/works-loop.php を探します。ハイフンが自動で入るので、ファイル名にハイフン、引数にはアンダースコアなしという対応を間違えないでください。

タクソノミーアーカイブは、term_description() を出せる形にしておくと後で効きます。「製造業の実績」というページに200字程度の説明文が入るだけで、業種名だけのページより検索結果での見え方が変わります。説明文は管理画面の「業種を編集」から入力でき、コードの変更は要りません。納品時に「ここに説明を入れられます」と一言添えておくと、運用側が使ってくれます。


一覧の見た目をCSSで整える

外部ライブラリは不要です。style.css(子テーマなら子テーマ側)に追記します。

/* 実績一覧 */
.works-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 32px 24px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.works-card {
    display: block;
    color: inherit;
    text-decoration: none;
}

.works-card__thumb {
    aspect-ratio: 4 / 3;
    overflow: hidden;
    background: #f2f2f0;
    border-radius: 6px;
}

.works-card__thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform .4s ease;
}

.works-card:hover .works-card__thumb img {
    transform: scale(1.04);
}

.works-card__cat {
    display: inline-block;
    margin-top: 14px;
    font-size: 12px;
    letter-spacing: .04em;
    color: #6b6b66;
}

.works-card__title {
    margin: 6px 0 0;
    font-size: 17px;
    line-height: 1.6;
    font-weight: 600;
}

.works-card__excerpt {
    margin: 8px 0 0;
    font-size: 14px;
    line-height: 1.9;
    color: #6b6b66;
}

/* 絞り込みリンク */
.works-filter__list {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 0 0 40px;
    padding: 0;
    list-style: none;
}

.works-filter__link {
    display: inline-block;
    padding: 7px 16px;
    border: 1px solid #d8d8d4;
    border-radius: 999px;
    font-size: 13px;
    text-decoration: none;
    color: inherit;
}

.works-filter__link.is-current,
.works-filter__link:hover {
    background: #1c1c1a;
    border-color: #1c1c1a;
    color: #fff;
}

変更する箇所: minmax(280px, 1fr) のカード最小幅、#f2f2f0 #6b6b66 #1c1c1a の各色、aspect-ratio: 4 / 3 の比率。

grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)) はメディアクエリなしで列数が変わります。画面幅900pxなら3列、600pxなら2列、400pxなら1列。ブレークポイントを書かずに済むので、追加のCSSが減ります。aspect-ratio は Chrome / Edge / Firefox / Safari のいずれも 2021年以降のバージョンで対応済みで、いま実務で気にする必要はありません。

アイキャッチ未設定の記事があると、そこだけカードの高さが崩れます。.works-card__noimage に最低限の見た目を持たせておくと、運用中に画像を入れ忘れても一覧が破綻しません。

.works-card__noimage {
    display: block;
    width: 100%;
    height: 100%;
    background: repeating-linear-gradient(45deg, #f2f2f0, #f2f2f0 8px, #ebebe8 8px, #ebebe8 16px);
}

JavaScriptによる絞り込みも作れますが、まずはリンクによる絞り込み(=タクソノミーアーカイブへの遷移)を推奨します。URLが変わるので検索エンジンに個別のページとして認識され、「業種+実績」の検索に対応できます。JSでDOMを出し分けると、その利点が消えます。

同じ考え方をShopifyで実装する手順は、こちらにまとめています。


パーマリンクの設計と、あとから変えたときに起きること

決めるべき3つ

URL設計で先に決める3点
投稿タイプ名DB保存値。変更=データ移行が必要
rewrite slugURLに出る文字列。変更=リダイレクト必須
タクソノミー slug絞り込みURLの階層。変更=同上

この3つは公開後に変えると必ず何かが壊れます。着手前に確定させてください。

投稿タイプ名を変えると何が起きるか

register_post_type( 'works', ... ) の works は、wp_posts テーブルの post_type カラムにそのまま保存されます。これを case に変えると、既存の記事は post_type = 'works' のまま残り、どの管理画面にも表示されなくなります。 データは消えていませんが、画面からは消えたように見えます。

どうしても変える必要が出たら、SQLで一括更新します。必ずバックアップを取ってから実行してください。

UPDATE wp_posts SET post_type = 'case' WHERE post_type = 'works';

wp_ の部分はインストール時のテーブル接頭辞に合わせます。加えて、タクソノミーとの紐づけ(wp_term_relationships)は投稿IDで結ばれているので、この更新だけで維持されます。ただしメタボックスの表示条件やACFのフィールドグループの「表示条件」は投稿タイプ名で判定しているので、そちらの設定も直す必要があります。

WP-CLIが使える環境なら、こちらのほうが安全です。

wp post list --post_type=works --format=ids | xargs -d ' ' -I % wp post update % --post_type=case

rewrite slug を変えると何が起きるか

URLが変わるので、旧URLは404になります。公開済みサイトなら301リダイレクトが必須です。プラグインを入れずに済ませるなら functions.php に書けます。

/**
 * 旧URL /case/ を /works/ へ301リダイレクトする
 */
function my_redirect_old_works_url() {
    if ( is_admin() ) {
        return;
    }
    $request = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
    if ( 0 === strpos( $request, '/case/' ) ) {
        $new = preg_replace( '#^/case/#', '/works/', $request );
        wp_safe_redirect( home_url( $new ), 301 );
        exit;
    }
}
add_action( 'template_redirect', 'my_redirect_old_works_url' );

変更する箇所: /case/(旧スラッグ)と /works/(新スラッグ)の2か所ずつ。

template_redirect フックを使うのは、WordPressのクエリ解決が終わったあとで、まだ出力が始まっていないタイミングだからです。init で書くと管理画面のAJAXまで巻き込みます。wp_redirect ではなく wp_safe_redirect を使うのは、リダイレクト先を自サイト内に制限するためです。

リダイレクトを入れたら、Search Consoleでインデックスの移行を確認します。計測まわりの確認手順はこちらに整理しています。

固定ページとの衝突

rewrite => array( 'slug' => 'works' ) を設定した状態で、スラッグが works の固定ページも作ると衝突します。 どちらが表示されるかは登録順に依存し、環境によって挙動が変わります。

「実績一覧ページを固定ページとして作り、そこにカスタム投稿の一覧を出す」という設計を選ぶ人がいますが、has_archive => true があればアーカイブが一覧そのものなので、固定ページは不要です。固定ページを作るなら投稿タイプ側の has_archive を false にして、二重にしないでください。


更新する人が迷わない管理画面にする

ここを詰めるかどうかで、納品後の問い合わせ件数が変わります。

一覧に列を足す

管理画面の実績一覧に、アイキャッチの有無を出しておくと「画像を入れ忘れた記事」がひと目でわかります。

/**
 * 管理画面の実績一覧にアイキャッチ列を追加する
 */
function my_works_admin_columns( $columns ) {
    $new = array();
    foreach ( $columns as $key => $label ) {
        if ( 'title' === $key ) {
            $new['works_thumb'] = 'サムネイル';
        }
        $new[ $key ] = $label;
    }
    return $new;
}
add_filter( 'manage_works_posts_columns', 'my_works_admin_columns' );

function my_works_admin_column_content( $column, $post_id ) {
    if ( 'works_thumb' !== $column ) {
        return;
    }
    if ( has_post_thumbnail( $post_id ) ) {
        echo get_the_post_thumbnail( $post_id, array( 60, 60 ), array( 'style' => 'object-fit:cover;border-radius:4px;' ) );
    } else {
        echo '<span style="color:#c00;">未設定</span>';
    }
}
add_action( 'manage_works_posts_custom_column', 'my_works_admin_column_content', 10, 2 );

フック名の manage_works_posts_columns の works が投稿タイプ名です。投稿タイプ名を変えたらここも変わります。

編集画面に注意書きを出す

アイキャッチの推奨サイズや、タイトルの文字数目安を編集画面に直接出しておくと、口頭やマニュアルで伝えるより確実に守られます。

/**
 * 実績の編集画面に運用ルールを表示する
 */
function my_works_editor_notice() {
    $screen = get_current_screen();
    if ( ! $screen || 'works' !== $screen->post_type || 'post' !== $screen->base ) {
        return;
    }
    ?>
    <div class="notice notice-info">
        <p><strong>実績を登録するときの決めごと</strong></p>
        <p>アイキャッチは横1200px以上・4:3を推奨します。タイトルは30文字以内に収めると一覧で改行されません。業種は必ず1つ選んでください。</p>
    </div>
    <?php
}
add_action( 'admin_notices', 'my_works_editor_notice' );

変更する箇所: works(投稿タイプ名)と、注意書きの本文。

'post' !== $screen->base の条件を入れているのは、一覧画面ではなく編集画面だけに出すためです。この行を外すと一覧にも出て、うるさくなります。

使わない機能を隠す

実績にコメント欄は要りません。supports から外していれば編集画面には出ませんが、投稿タイプ登録時に指定を忘れていた場合は remove_post_type_support で外せます。

function my_works_remove_supports() {
    remove_post_type_support( 'works', 'comments' );
    remove_post_type_support( 'works', 'trackbacks' );
}
add_action( 'init', 'my_works_remove_supports', 20 );

add_action の優先度を 20 にしているのは、投稿タイプの登録(既定の優先度10)より後に実行するためです。同じ優先度だと登録前に実行されて効きません。

編集者が触る画面を整える発想は、ブロックパターンでも同じです。


ブロックテーマの場合

register_post_type の書き方は変わりません。変わるのはテンプレートの置き場所です。

クラシックテーマ ブロックテーマ
archive-works.php templates/archive-works.html
single-works.php templates/single-works.html
taxonomy-work_cat.php templates/taxonomy-work_cat.html

ブロックテーマではPHPではなくブロックマークアップ(HTMLコメント形式)を書きます。ただし実務では、サイトエディターで「実績アーカイブ」テンプレートを作り、そこでクエリループブロックを配置してからエクスポートするほうが早いです。手書きするより確実です。

templates/archive-works.html の最小構成は次のようになります。

<!-- wp:template-part {"slug":"header","tagName":"header"} /-->

<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
    <!-- wp:query-title {"type":"archive","level":1} /-->

    <!-- wp:query {"queryId":1,"query":{"perPage":12,"postType":"works","inherit":true},"layout":{"type":"default"}} -->
    <div class="wp-block-query">
        <!-- wp:post-template {"layout":{"type":"grid","columnCount":3}} -->
            <!-- wp:post-featured-image {"isLink":true,"aspectRatio":"4/3"} /-->
            <!-- wp:post-terms {"term":"work_cat"} /-->
            <!-- wp:post-title {"isLink":true,"level":2} /-->
            <!-- wp:post-excerpt {"excerptLength":60} /-->
        <!-- /wp:post-template -->

        <!-- wp:query-pagination -->
            <!-- wp:query-pagination-previous /-->
            <!-- wp:query-pagination-numbers /-->
            <!-- wp:query-pagination-next /-->
        <!-- /wp:query-pagination -->
    </div>
    <!-- /wp:query -->
</main>
<!-- /wp:group -->

<!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->

"inherit":true は「そのページの本来のクエリを引き継ぐ」という指定です。アーカイブテンプレートではこれを true にしておかないと、絞り込みが効きません。columnCount と perPage が変更箇所です。

aspectRatio の指定はWordPress 6.3以降で使えます。それより古い環境ではCSS側で指定してください。バージョンは管理画面のダッシュボード右下、または「ツール → サイトヘルス → 情報」で確認できます。

ブロックテーマを選ぶかクラシックテーマを選ぶかは、更新する人が誰かで決めてください。クライアントがサイトエディターで一覧の見た目まで触れる状態にすると、崩れたときに戻せません。逆に、社内の編集者が細かく調整したいならブロックテーマのほうが速い。カスタム投稿の一覧に限れば、レイアウトを固定したいケースが多いので、クラシックテーマ+PHPテンプレートのほうが破綻しにくいというのが実感です。


うまくいかないとき

/works/ が404になる

3つのどれかです。

  1. パーマリンクを再保存していない — 設定 → パーマリンク → 変更を保存。これが最頻出です
  2. has_archive が false または未指定 — 既定値は false です。明示的に true を書いてください
  3. 固定ページとスラッグが衝突している — works という固定ページがないか確認

一覧に記事が出ない

  • 記事のステータスが下書き — 一覧はデフォルトで公開済みのみ表示します
  • pre_get_posts の条件ミス — is_post_type_archive( 'works' ) の投稿タイプ名を確認
  • is_main_query() を書き忘れて別のクエリを潰している — 他のプラグインのクエリまで書き換わっている可能性があります

管理画面のメニューに出てこない

'public' => true があるか確認してください。public を false にすると show_ui も false になり、管理画面から消えます。フロントに出したくないが管理画面では編集したい場合は 'public' => false, 'show_ui' => true の組み合わせです。

タクソノミーの絞り込みページが404

投稿タイプと同じくパーマリンクの再保存が必要です。それでも直らない場合、register_taxonomy が register_post_type より先に走っていないか確認します。どちらも init フックで同じ優先度なら、add_action を書いた順に実行されるので、投稿タイプの登録を先に書いてください。

記事のスラッグが works-2 のような連番になる

同じスラッグの固定ページや投稿がすでに存在しています。WordPressは投稿タイプが違っても、同じ階層のURLで衝突すると自動的に連番を足します。固定ページ側のスラッグを変えるのが確実です。

ブロックエディターにならない

'show_in_rest' => true が抜けています。この値は名前のとおりREST APIの有効化ですが、ブロックエディターがREST API経由で動くため、実質的にエディターの切り替えスイッチになっています。

検索結果にカスタム投稿が出てこない

サイト内検索は既定で post と page しか対象にしません。カスタム投稿タイプを含めるには exclude_from_search を false にした上で(public => true なら既定でそうなります)、検索クエリの post_type を明示します。

function my_include_works_in_search( $query ) {
    if ( is_admin() || ! $query->is_main_query() || ! $query->is_search() ) {
        return;
    }
    $query->set( 'post_type', array( 'post', 'page', 'works', 'news' ) );
}
add_action( 'pre_get_posts', 'my_include_works_in_search' );

post_type を配列で渡すと、指定したものだけが対象になります。works を足したつもりで post を書き忘れると、通常の投稿が検索から消えます。

Cannot modify header information のエラー

リダイレクト処理で出やすいエラーです。functions.php の先頭に空行やBOMが混ざっていないか確認してください。PHPファイルの最後の ?> は書かないのが安全です(後ろに改行が入ると出力が始まります)。

変更が反映されない

サーバーやプラグインのキャッシュが効いています。キャッシュ系プラグインを一時停止するか、シークレットウィンドウで確認してください。CDNを挟んでいる場合はそちらのパージも必要です。


よくある質問

カスタム投稿タイプはプラグインで作るべきですか、コードで書くべきですか

一覧テンプレートまで自分で作るなら、コードで書くほうが管理しやすいです。投稿タイプの定義とテンプレートが同じリポジトリで管理でき、環境間の差分が出ません。プラグインのGUIで作ると設定がDBに保存されるため、開発環境と本番でズレます。ただし、非エンジニアが後から投稿タイプを追加する運用ならプラグインが向きます。

投稿タイプ名は日本語でも登録できますか

やめてください。register_post_type の第1引数はDBに保存され、URLやフック名にも使われます。英小文字・数字・アンダースコアのみ、20文字以内という制約があります。管理画面に出る日本語は labels 側で指定するので、表示だけ日本語にすれば十分です。

通常の投稿にカテゴリーを足す方式と、どちらが良いですか

更新する人が別々なら、カスタム投稿タイプで分けてください。実績とお知らせを同じ post に入れると、一覧テンプレートで毎回カテゴリー除外の条件を書く必要があり、除外漏れが起きます。件数がごく少なく今後も増えないなら、固定ページで手書きするほうが早いこともあります。

一覧をJavaScriptで絞り込むのと、タクソノミーアーカイブへのリンクとどちらが良いですか

原則はリンクです。URLが分かれるので検索エンジンに個別ページとして認識され、ページ遷移の履歴も残ります。件数が数十件で、絞り込みを何度も切り替える使い方を想定する場合のみ、JSで表示を出し分ける価値があります。その場合も初期表示は全件をHTMLに出しておき、JSは表示切り替えだけを担当させてください。

あとから投稿タイプ名を変えたくなったらどうしますか

wp_posts テーブルの post_type をSQLかWP-CLIで一括更新します。バックアップを取ってから実行してください。加えて、ACFのフィールドグループの表示条件、manage_◯◯_posts_columns のようなフック名、テンプレートのファイル名(archive-works.php)もすべて追随させる必要があります。手間が大きいので、着手前に名前を確定させるのが最善です。

カスタムフィールド(ACF)は使ったほうがいいですか

一覧カードに「クライアント名」「制作年」「担当領域」のような定型項目を出すなら使ってください。本文エディターに書かせると、書式が人によってばらつき、一覧の見た目が揃いません。ACFを使う場合、フィールドグループの表示条件を投稿タイプ works に設定し、一覧テンプレートでは get_post_meta() か get_field() で取り出します。プラグイン依存を避けたいなら add_meta_box で自作もできますが、保存処理とnonceの検証を自前で書く必要があるので、項目数が多いならACFのほうが早く安全です。


まとめ

カスタム投稿タイプの実装は、実質的に次の5ステップです。

  1. register_post_type で箱を作る(has_archive => true と with_front => false を忘れない)
  2. register_taxonomy で分類軸を足す(更新者が触るものは hierarchical => true)
  3. パーマリンク設定を再保存する(ここを飛ばすと404)
  4. archive-works.php を置いて一覧を作る(テンプレート階層に従うだけ)
  5. pre_get_posts で件数を調整する(is_admin() と is_main_query() の2行が必須)

一番割に合わないのは、公開後に投稿タイプ名やスラッグを変えることです。データ移行とリダイレクトの手間に加えて、検索順位の回復にも時間がかかります。着手前に「この箱の名前は何か」「URLはどうなるか」の2つだけ、クライアントと合意しておいてください。それさえ決まっていれば、実装そのものは短時間で終わります。

WordPressテーマの選定から悩んでいる場合は、こちらも参考にしてください。