この記事の対象: WordPressで「導入事例」「制作実績」ページを作る制作者・社内担当者
読了時間: 約12分
「wordpress 事例」で検索する人は、だいたい二手に分かれます。WordPressで作られたサイトのデザインを眺めたい人と、自社サイトに事例ページを増やしたい人。前者は見れば済みますが、後者は投稿タイプの設計から絞り込みの実装まで決めることが多く、途中で止まりがちです。
この記事は後者を正面から扱います。固定ページで1枚ずつ作る方式の限界、カスタム投稿タイプへの切り替え方、業種・課題での絞り込み、一覧カードのCSS。コードはコピペして動く形で載せています。あわせて、事例ページを「読まれる形」に整えるときの構成の型も、Soreiine!!ギャラリー掲載サイトの作りから拾って紹介します。
WordPressの事例ページは3つの作り方がある

最初に分岐を潰しておきます。事例ページの作り方は実質3通りで、記事数の見通しで決まります。
| 方式 | 向く件数 | 絞り込み | 更新の手間 |
|---|---|---|---|
| 固定ページを1枚ずつ | 〜5件 | 不可 | 毎回テンプレ複製 |
| 通常投稿+カテゴリー | 〜20件 | カテゴリーのみ | ブログと混ざる |
| カスタム投稿タイプ+タクソノミー | 20件〜 | 業種・課題で多軸 | 項目を埋めるだけ |
固定ページ方式は、3件で始めて5件で崩れます。崩れる原因は件数ではなく「並び替えができない」ことです。固定ページには公開日が効かないため、新しい事例を上に出したいだけで menu_order を手で振り直す作業が発生します。
通常投稿に混ぜる方式も、一見楽ですが後で詰みます。/blog/ の新着に事例が割り込み、RSSにも流れ、カテゴリー1軸しか持てません。「製造業の事例」と「採用課題の事例」を両方の切り口で出したい、という要望はほぼ必ず出てきます。
カスタム投稿タイプで作る
- 業種・課題・規模を別軸で持てる
- 一覧テンプレートが1枚で済む
- 入力項目を固定できる
- ブログと動線が混ざらない
固定ページ/通常投稿で作る
- 軸は実質1つまで
- 件数が増えると並び替えが手作業
- 書き手によって構成がばらつく
- 新着に事例が割り込む
将来20件を超える見込みがあるなら、最初からカスタム投稿タイプで作るほうが安いです。10件作ってから移すと、URLの移行とリダイレクトの手当てが別途発生します。
「事例」という言葉を先に定義しておく
実装前に社内で揃えておくと手戻りが減ります。「事例」が指すものは会社によって違い、少なくとも3種類あります。
- 導入事例 — 顧客にインタビューした記事。顔写真・社名・コメントが入る
- 制作実績 — 自社が作ったもののショーケース。ビジュアルが主役
- 施工・納入事例 — 件数を積む前提。写真と仕様の列挙が中心
導入事例はテキストが長く読み物、制作実績は画像が主役でグリッド表示、施工事例は検索性が命。この3つは必要なフィールドも一覧の形も違うので、1つの投稿タイプに押し込むと入力画面が空欄だらけになります。種類をまたぐなら投稿タイプを分けてください。
カスタム投稿タイプで事例を作る実装コード

ここからコードです。テーマの functions.php に直接書かず、プラグイン化またはミューテーマの mu-plugins に置くのを推奨します。理由は、投稿タイプがテーマに紐づくと、テーマを変えた瞬間に管理画面から事例が消えて見えるからです(データは残りますが編集できなくなります)。
投稿タイプとタクソノミーの登録
<?php
/**
* Plugin Name: Case Studies
* Description: 導入事例のカスタム投稿タイプとタクソノミー
*/
add_action( 'init', 'mysite_register_case_post_type' );
function mysite_register_case_post_type() {
register_post_type(
'case',
array(
'label' => '導入事例',
'public' => true,
'has_archive' => true,
'menu_icon' => 'dashicons-portfolio',
'menu_position' => 5,
'rewrite' => array(
'slug' => 'case',
'with_front' => false,
),
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt', 'revisions' ),
'show_in_rest' => true, // ブロックエディタを使うなら必須
)
);
register_taxonomy(
'case_industry',
'case',
array(
'label' => '業種',
'public' => true,
'hierarchical' => true, // チェックボックスで選ばせる
'show_admin_column' => true,
'rewrite' => array(
'slug' => 'case/industry',
'with_front' => false,
),
'show_in_rest' => true,
)
);
register_taxonomy(
'case_issue',
'case',
array(
'label' => '課題',
'public' => true,
'hierarchical' => true,
'show_admin_column' => true,
'rewrite' => array(
'slug' => 'case/issue',
'with_front' => false,
),
'show_in_rest' => true,
)
);
}
実装のポイント
with_front => false を入れているのは、パーマリンク設定で先頭に /blog/ などが付いている環境で /blog/case/ になってしまうのを防ぐためです。これを忘れると、公開後にURLが変わって301が必要になります。
hierarchical => true は見た目の都合です。false(タグ形式)にすると管理画面が自由入力欄になり、「製造業」と「製造」が混在します。事例の軸は選択式にしてください。
show_in_rest => true を落とすと、ブロックエディタでそのタクソノミーのパネルが出ません。クラシックエディタのみの環境では省略できますが、入れておいて損はありません。
つまずきやすい点:404になる
登録直後に /case/ を開くと404が出ます。リライトルールがキャッシュされているためで、プラグインを有効化し直すか、管理画面の「設定 → パーマリンク」を開くだけで解決します(保存ボタンを押さなくても flush_rewrite_rules() が走ります)。
本番で確実にやるなら、有効化フックに入れておきます。
register_activation_hook( __FILE__, function () {
mysite_register_case_post_type();
flush_rewrite_rules();
} );
flush_rewrite_rules() を init で毎回呼ぶ記事を見かけますが、これは毎リクエストでルールを再生成するので入れないでください。表示が目に見えて遅くなります。
事例に入れる項目をフィールドで固定する
事例が複数人で書かれると、構成が毎回変わります。「課題 → 施策 → 結果」の3点を本文に自由記述させると、3件目から崩れます。項目を分けて持たせたほうが、一覧でも抜粋に使えて便利です。
プラグインなしで足りる範囲なら、ブロックエディタ側にメタボックスを足すよりも、投稿テンプレート(template)で型を固定するほうが手軽です。
// register_post_type の引数に追加
'template' => array(
array( 'core/heading', array( 'level' => 2, 'content' => '課題' ) ),
array( 'core/paragraph', array( 'placeholder' => '導入前に何が起きていたか' ) ),
array( 'core/heading', array( 'level' => 2, 'content' => '実施したこと' ) ),
array( 'core/paragraph', array( 'placeholder' => '施策を時系列で' ) ),
array( 'core/heading', array( 'level' => 2, 'content' => '結果' ) ),
array( 'core/paragraph', array( 'placeholder' => '数字か、担当者の言葉で' ) ),
),
'template_lock' => 'insert', // 見出しの削除を禁止し、追加は許可
新規作成時に見出しが入った状態で開きます。template_lock は 'all'(完全固定)、'insert'(既存ブロックの並べ替え・削除を禁止)、未指定(自由)の3択です。社内の書き手が複数いるなら 'insert' が落ち着きます。
対応バージョン: template はブロックエディタ環境(WordPress 5.0以降)で動作します。クラシックエディタでは無視されます。template_lock の 'insert' は WordPress 5.9以降。PHP 7.4〜8.4で確認。
事例一覧を業種と課題で絞り込む

ここが実装の山場です。件数が増えると、一覧を上から見る人はいなくなります。
アーカイブの表示件数と並び順を変える
add_action( 'pre_get_posts', 'mysite_case_archive_query' );
function mysite_case_archive_query( $query ) {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
// 事例アーカイブと、業種・課題のターム一覧の両方に効かせる
if ( $query->is_post_type_archive( 'case' ) || $query->is_tax( array( 'case_industry', 'case_issue' ) ) ) {
$query->set( 'posts_per_page', 12 );
$query->set( 'orderby', 'date' );
$query->set( 'order', 'DESC' );
}
}
つまずきやすい点:is_admin() と is_main_query() のガードを落とすと、管理画面の投稿一覧やサイドバーのウィジェットにも条件が漏れます。「管理画面の事例一覧が12件しか出ない」という症状はほぼこれです。
2軸の絞り込みをGETパラメータで受ける
業種と課題をAND条件で掛け合わせたいとき、タクソノミーのアーカイブURLだけでは表現できません。/case/?industry=manufacturing&issue=recruit の形で受け取る実装にします。
add_action( 'pre_get_posts', 'mysite_case_filter_query' );
function mysite_case_filter_query( $query ) {
if ( is_admin() || ! $query->is_main_query() || ! $query->is_post_type_archive( 'case' ) ) {
return;
}
$tax_query = array();
foreach ( array( 'industry' => 'case_industry', 'issue' => 'case_issue' ) as $param => $taxonomy ) {
if ( empty( $_GET[ $param ] ) ) {
continue;
}
$slug = sanitize_title( wp_unslash( $_GET[ $param ] ) );
// 存在しないタームが渡されたら無視する
if ( ! term_exists( $slug, $taxonomy ) ) {
continue;
}
$tax_query[] = array(
'taxonomy' => $taxonomy,
'field' => 'slug',
'terms' => $slug,
);
}
if ( count( $tax_query ) > 1 ) {
$tax_query['relation'] = 'AND';
}
if ( $tax_query ) {
$query->set( 'tax_query', $tax_query );
}
}
なぜこう書くのか
sanitize_title() を通しているのは、GETの値をそのままクエリに渡さないためです。tax_query はWordPress側でプレースホルダ化されるので直接のSQLインジェクションは起きませんが、不正な値で意図しない結果が出るのは避けたい。term_exists() で存在確認までしているのは、typoされたURLで「0件です」と表示するより、全件に戻したほうが離脱しないという判断です。
AND条件とOR条件はどちらが正しいとも言えません。件数が少ないサイトでANDにすると0件が頻発するので、総数が30件を切るなら relation は 'OR' から始めて、増えてからANDに寄せるのが無難です。
絞り込みUIをテンプレートに置く
archive-case.php の冒頭に入れるフォームです。選択中の状態を保持します。
<?php
$taxonomies = array(
'industry' => array( 'tax' => 'case_industry', 'label' => '業種' ),
'issue' => array( 'tax' => 'case_issue', 'label' => '課題' ),
);
?>
<form class="case-filter" method="get" action="<?php echo esc_url( get_post_type_archive_link( 'case' ) ); ?>">
<?php foreach ( $taxonomies as $param => $config ) : ?>
<?php
$terms = get_terms(
array(
'taxonomy' => $config['tax'],
'hide_empty' => true,
)
);
if ( is_wp_error( $terms ) || empty( $terms ) ) {
continue;
}
$current = isset( $_GET[ $param ] ) ? sanitize_title( wp_unslash( $_GET[ $param ] ) ) : '';
?>
<p class="case-filter__group">
<label for="case-<?php echo esc_attr( $param ); ?>"><?php echo esc_html( $config['label'] ); ?></label>
<select id="case-<?php echo esc_attr( $param ); ?>" name="<?php echo esc_attr( $param ); ?>">
<option value="">すべて</option>
<?php foreach ( $terms as $term ) : ?>
<option value="<?php echo esc_attr( $term->slug ); ?>" <?php selected( $current, $term->slug ); ?>>
<?php echo esc_html( $term->name ); ?>(<?php echo (int) $term->count; ?>)
</option>
<?php endforeach; ?>
</select>
</p>
<?php endforeach; ?>
<button type="submit">絞り込む</button>
<?php if ( ! empty( $_GET['industry'] ) || ! empty( $_GET['issue'] ) ) : ?>
<a href="<?php echo esc_url( get_post_type_archive_link( 'case' ) ); ?>">条件をクリア</a>
<?php endif; ?>
</form>
hide_empty => true で0件のタームを隠し、$term->count で件数を併記しています。これを入れるだけで「選んだら0件だった」が起きません。選択肢に件数が出ているサイトは、押す前に結果が読めるので操作が止まりません。
JavaScriptでの即時絞り込み(fetch で置き換える形)にしたくなりますが、まずこのフォーム送信型で出すことを勧めます。URLに条件が乗るので共有でき、検索エンジンにも拾われ、JSが落ちても動きます。非同期化は体験の改善幅が小さいのに、履歴管理とフォーカス移動の実装が増えます。
一覧カードのCSS
グリッドは auto-fill で組むと、件数とブレークポイントの管理が消えます。
.case-list {
display: grid;
gap: clamp(1.25rem, 3vw, 2.5rem);
grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr));
list-style: none;
margin: 0;
padding: 0;
}
.case-card {
display: flex;
flex-direction: column;
background: #fff;
border-radius: 0.5rem;
overflow: hidden;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.08);
transition: box-shadow 0.2s ease, transform 0.2s ease;
}
.case-card:hover,
.case-card:focus-within {
box-shadow: 0 8px 24px rgb(0 0 0 / 0.12);
transform: translateY(-2px);
}
.case-card__thumb {
aspect-ratio: 16 / 10;
object-fit: cover;
width: 100%;
display: block;
}
.case-card__body {
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
flex: 1;
}
.case-card__title {
font-size: 1.0625rem;
line-height: 1.6;
word-break: keep-all;
overflow-wrap: anywhere;
}
.case-card__meta {
margin-top: auto;
font-size: 0.8125rem;
color: #6b7280;
}
@media (prefers-reduced-motion: reduce) {
.case-card {
transition: none;
}
.case-card:hover {
transform: none;
}
}
なぜこう書くのか
minmax(min(100%, 17rem), 1fr) の内側の min() は、スマホで横スクロールが出るのを防ぐためです。minmax(17rem, 1fr) だけだと、画面幅が17remを下回る端末でカードがはみ出します。
:focus-within を :hover と並べているのは、キーボード操作でカードを辿ったときに同じ見た目になるようにするため。ホバーだけ書いてフォーカスを忘れる実装は非常に多く、Tabで移動しても一覧のどこを見ているか分からなくなります。
word-break: keep-all は日本語の折り返しを単語単位に寄せる指定です。カードのタイトルは幅が狭いので、これを入れないと「導入事/例」のような切れ方が出ます。
対応ブラウザ: aspect-ratio・gap(Grid)・clamp()・:focus-within・min() はいずれもモダンブラウザで広くサポートされています。IEは対象外。prefers-reduced-motion も同様。
事例ページの構成は「読まれる順」で決まる

実装が終わっても、中身の順番が悪いと読まれません。Soreiine!!のギャラリーに掲載されているサイトを見ていくと、事例ページの作りは数パターンに収束しています。
図の通り、順番を「結果 → 素性 → 経緯」にするのが要点です。時系列で課題から書き始めると、読者は自分に関係あるかどうかを判断できないまま読み進めることになります。
ギャラリー掲載サイトから拾える共通点
見どころ1:事例のサムネイルに社名を入れている
一覧で写真だけを並べると、どれも同じに見えます。掲載サイトのうち評価が高いものは、サムネイル上に業種ラベルか社名を重ねており、グリッドをスクロールするだけで「自分と近い会社があるか」が分かります。実装としてはアイキャッチにテキストを焼き込むのではなく、CSSで重ねるほうが運用が楽です(社名変更に追従できる)。
見どころ2:事例から事例へ横移動できる
事例を読んだ人は、1件で判断しません。詳細ページ下部に「同じ業種の事例」を3件出しているサイトは、次の事例への遷移が起きやすくなります。これは get_posts() に同じタームを渡すだけで作れます。
<?php
$industry_terms = wp_get_post_terms( get_the_ID(), 'case_industry', array( 'fields' => 'ids' ) );
if ( ! is_wp_error( $industry_terms ) && $industry_terms ) :
$related = get_posts(
array(
'post_type' => 'case',
'posts_per_page' => 3,
'post__not_in' => array( get_the_ID() ),
'no_found_rows' => true, // ページネーション不要なのでCOUNTを省く
'tax_query' => array(
array(
'taxonomy' => 'case_industry',
'field' => 'term_id',
'terms' => $industry_terms,
),
),
)
);
if ( $related ) : ?>
<section class="case-related">
<h2>同じ業種の事例</h2>
<ul class="case-list">
<?php foreach ( $related as $post ) : setup_postdata( $post ); ?>
<li class="case-card"><!-- カードの中身 --></li>
<?php endforeach; wp_reset_postdata(); ?>
</ul>
</section>
<?php endif;
endif;
no_found_rows => true は見落としやすい指定です。関連記事のように総件数を使わないクエリでは、これを入れるだけで SELECT FOUND_ROWS() が走らなくなります。事例の件数が多いサイトでは体感差が出ます。
見どころ3:写真の縦横比が揃っている
一覧の印象を決めているのは、デザインよりも画像の統一感です。アイキャッチの比率が混ざっているサイトは、CSSでいくら object-fit: cover を当てても、トリミング位置がバラついて落ち着きません。入稿ルールとして比率を1つ決め、管理画面に推奨サイズを出しておくのが結局いちばん効きます。
add_filter( 'post_thumbnail_html', 'mysite_case_thumb_hint', 10, 5 );
// もしくは、投稿画面にシンプルに注意書きを出す
add_action( 'admin_notices', function () {
$screen = get_current_screen();
if ( $screen && 'case' === $screen->post_type && 'post' === $screen->base ) {
echo '<div class="notice notice-info"><p>アイキャッチは <strong>1600×1000px(16:10)</strong> で入稿してください。</p></div>';
}
} );
業種ごとの事例ページの傾向は、分類して並べると見えてきます。ギャラリーを業種で絞って見比べるのが手っ取り早いです。
レイアウトの型そのものを決めかねているなら、1カラムWebデザインレイアウトの実例8選も参考になります。事例詳細は縦読みが前提なので、1カラムの設計がそのまま当てはまります。
ブロックテーマで作る場合の差分

クラシックテーマ(PHPテンプレート)前提で書いてきましたが、ブロックテーマ(FSE)で作る場合の違いだけ押さえておきます。
| 項目 | クラシックテーマ | ブロックテーマ |
|---|---|---|
| 一覧テンプレート | archive-case.php |
templates/archive-case.html |
| 詳細テンプレート | single-case.php |
templates/single-case.html |
| 一覧の組み立て | while ( have_posts() ) |
クエリループブロック |
| 件数・並び順 | pre_get_posts |
ブロック設定+pre_get_posts |
| 絞り込みUI | 自作フォーム | 自作ブロックかPHPショートコード |
ブロックテーマでも pre_get_posts は有効です。ただしクエリループブロックが「継承」設定の場合のみメインクエリを使うので、ブロック側で投稿タイプを明示指定しているとフックが効きません。「コードを書いたのに件数が変わらない」ときは、まずブロックの設定が継承になっているか確認してください。
絞り込みUIについては、ブロックテーマでも素直にPHPで作ってショートコードかブロックパターンで差し込むのが早いです。ここをフルブロック化しようとすると、工数が大きく膨らみます。
テーマそのものを切り替える検討段階なら、先に費用の内訳を見ておくと判断しやすくなります。
公開後に効いてくる3つの設定

事例ページは作って終わりではなく、増やしていく前提の領域です。最初に入れておくと後で楽になるものを挙げます。
1. 事例詳細にパンくずとタクソノミーリンクを出す
/case/ → /case/industry/manufacturing/ → 詳細、の階層が検索エンジンに伝わります。タームアーカイブが実質的なまとめページになるので、業種名での流入が拾えるようになります。
2. アーカイブに noindex を付けるかを決めておく
絞り込み結果のURL(?industry=...&issue=...)は組み合わせ数だけ増えます。パラメータ付きのURLには noindex を出し、タームアーカイブ(/case/industry/xxx/)は index させるのが基本線です。
add_action( 'wp_head', function () {
if ( is_post_type_archive( 'case' ) && ( ! empty( $_GET['industry'] ) || ! empty( $_GET['issue'] ) ) ) {
echo '<meta name="robots" content="noindex,follow">' . "\n";
}
}, 1 );
follow を残しているのは、絞り込み結果から詳細ページへのリンクは辿らせたいからです。noindex,nofollow にすると詳細の発見経路をひとつ潰します。
3. 1件目を作る前にテンプレートを埋めておく
前述の template 指定です。1件目を自由に書いてしまうと、それが社内の見本になって2件目以降もばらつきます。
リニューアルのタイミングで事例を整えるなら、決めることの全体像を先に押さえておくと抜けが減ります。
よくある質問

WordPressの事例ページは固定ページで作ってはいけませんか
5件以内で今後も増えないなら固定ページで問題ありません。ただし公開日による並び替えが効かないため、新しい事例を上に出す運用になると手作業が発生します。20件を超える見込みがあるなら、最初からカスタム投稿タイプで作るほうが移行コストを払わずに済みます。
カスタム投稿タイプの登録はプラグインとfunctions.phpのどちらに書くべきですか
プラグイン、または mu-plugins を推奨します。functions.php に書くとテーマを切り替えた瞬間に投稿タイプが管理画面から消え、事例を編集できなくなります。データ自体はDBに残っているので復旧できますが、テーマ変更のたびに移植作業が必要になります。
事例アーカイブが404になります
リライトルールのキャッシュが原因です。管理画面の「設定 → パーマリンク」を開くと再生成されて解決します。プラグイン化している場合は register_activation_hook 内で flush_rewrite_rules() を呼んでおくと、有効化時に自動で処理されます。init で毎回呼ぶのは避けてください。
業種と課題の絞り込みはANDとORのどちらにすべきですか
掲載件数で決めます。総数が30件を下回るならORから始めてください。ANDにすると「製造業かつ採用課題」で0件になるケースが頻発し、絞り込みが使われなくなります。件数が増えて結果が多すぎるようになった時点でANDへ寄せます。
ブロックテーマでも pre_get_posts は使えますか
使えます。ただしクエリループブロックの設定が「テンプレートのクエリを継承」になっている場合に限ります。ブロック側で投稿タイプや件数を個別指定していると独自クエリになり、pre_get_posts の変更が反映されません。効かないときはまずブロックの継承設定を確認してください。
まとめ
WordPressで事例ページを作るときの判断は、ほぼ件数で決まります。20件を超える見込みがあるならカスタム投稿タイプ、軸が2つ以上必要ならタクソノミーを分けて tax_query で掛ける。この2つを最初に決めておけば、あとはテンプレートを1枚書くだけで増やし続けられます。
実装より難しいのは中身の順番です。結果を先に出し、相手の素性を明示し、最後に類似事例へ横移動させる。ギャラリーで評価されている事例ページはこの順を守っています。読まれる形が決まってから、絞り込みやカードのCSSを整えるのが正しい順番です。
デザインの型を探している段階なら、業種別の掲載事例を並べて比べるのが近道です。




