IMPLEMENTATION SPEC / WORDPRESS
静的HTMLで作ったデザインを、クライアント様ご自身で更新できる形にするための設計書です。テーマ構成の選択肢、データ設計、検索の実装、セキュリティ、納品前の確認項目までを1枚にまとめています。
物件情報は固定ページや投稿では管理しません。カスタム投稿タイプ+カスタムフィールドで構造化し、賃料・面積・駅徒歩を数値として持ちます。こうしておかないと、あとから「賃料が安い順」の並べ替えができません。
また、カスタム投稿タイプの登録はテーマの functions.php ではなくプラグインに書きます。テーマを入れ替えたときに、登録した投稿タイプごと消えて物件が全部見えなくなるためです。
2〜3年後にデザインをリニューアルするとき、データはそのままでテーマだけ差し替えられます。逆にテーマに書いてしまうと、リニューアルのたびにデータ移行が必要になります。
ご予算とご希望の動きの量によって、3つの選び方があります。
| 構成 | できること | 更新耐性 | 工数 | 費用の目安 |
|---|---|---|---|---|
| A. 既存テーマそのまま SWELL / Snow Monkey 等 |
テーマが用意している範囲のデザインと、フェードイン程度の動き | 高 | 3〜5人日 | 45,000円〜 |
| B. 子テーマ+独自テンプレート トップとLPだけ独自 |
主要ページは自由に作り込み、下層は親テーマの資産を使う | 中 | 9〜13人日 | 75,000〜105,000円 |
| C. オリジナルテーマ | 全ページを構造から設計。パララックス・SVGアニメーション・Canvasを自由に組める | 高 | 18〜24人日 | 132,000円〜 |
親テーマのHTML構造が変わると、子テーマで上書きしたCSSがずれることがあります。親テーマのメジャーアップデート時には確認が必要です。この確認作業を保守契約に含めるかどうかを、契約時に決めておくことをお勧めします。
ご提案としてはBを推します。予算あたりの効果が最も高く、更新頻度の高いトップページに投資を集中できます。全ページに動きを入れたい場合はCになります。
サンプルで作った動きを、それぞれの構成で実装できるかどうかの一覧です。
| 実装したい動き | A 既存テーマ | B 子テーマ+独自 | C オリジナル | 備考 |
|---|---|---|---|---|
| フェードイン・スライドイン | 可 | 可 | 可 | 多くのテーマに標準機能あり |
| スクロール連動パララックス | 条件付 | 可 | 可 | Aは背景固定程度。多層は構造に手が要る |
| SVGの線画アニメーション | 不可 | 可 | 可 | SVGをテンプレートに直接書く必要がある |
| Canvasのアニメーション | 不可 | 可 | 可 | 独自JSの読み込みが必須 |
| 横スクロールのタイムライン | 不可 | 可 | 可 | 専用テンプレートが要る |
| 絞り込み検索(本サンプルの機能) | プラグイン | 可 | 可 | Aは検索プラグイン依存。デザイン自由度は下がる |
| スクロールスパイ目次 | プラグイン | 可 | 可 | — |
Elementor等のアニメーション機能はフェードイン系に限られ、上表の下4行は実現できません。加えて、ビルダーが生成するDOMは階層が深く、読み込むCSS/JSが二重になって表示速度が落ちます。集客目的のサイトでは、速度低下が検索順位とCVRの両方に効きます。
| 項目 | 内容 | 工数 |
|---|---|---|
| パララックス実装 | 多層の背景、スクロール連動、reduced-motion対応 | 1.5人日 |
| SVGアニメーション | 図版の作図+線画アニメーション(1点あたり) | 1.0人日 |
| Canvas描画 | データ連動の図(1点あたり) | 2.0人日 |
| 横スクロール実装 | スクロール量の計算、リサイズ対応、代替表示 | 1.5人日 |
| 速度計測と調整 | Core Web Vitals の計測、遅延読み込み、再計測 | 1.0人日 |
// プラグイン側で登録する。テーマの functions.php には書かない
add_action( 'init', function () {
register_post_type( 'property', [
'label' => '物件',
'public' => true,
'has_archive' => true,
'menu_icon' => 'dashicons-admin-home',
'supports' => [ 'title', 'editor', 'thumbnail', 'revisions' ],
'show_in_rest' => true,
'rewrite' => [ 'slug' => 'property', 'with_front' => false ],
] );
register_taxonomy( 'area', 'property', [
'label' => 'エリア',
'hierarchical' => true,
'show_in_rest' => true,
] );
} );
| キー | 型 | 用途 | 備考 |
|---|---|---|---|
| rent | 整数(円) | 賃料 | 「12.5万円」ではなく 125000 で持つ |
| size_sqm | 小数 | 専有面積 | 並べ替えに使う |
| walk_min | 整数 | 駅徒歩 | 同上 |
| layout | 文字列 | 間取り | 1R / 1K / 1LDK … を選択式に |
| station | 文字列 | 最寄駅 | — |
| features | 配列 | こだわり条件 | タクソノミーでも可 |
「12.5万円」のような表示用の文字列で保存すると、並べ替えも範囲検索もできなくなります。表示は出力時に整形します。これは後から直すのが最も高くつく設計ミスです。
URLのクエリ文字列で条件を受け取り、pre_get_posts でメインクエリを書き換えます。検索結果がURLで共有できる状態になります。
add_action( 'pre_get_posts', function ( $q ) {
if ( is_admin() || ! $q->is_main_query() || ! $q->is_post_type_archive( 'property' ) ) {
return;
}
// 並べ替えは許可リスト方式。$_GET をそのまま orderby に渡さない
$allowed = [ 'rent' => 'ASC', 'size_sqm' => 'DESC', 'walk_min' => 'ASC' ];
$key = sanitize_key( $_GET['sort'] ?? '' );
if ( isset( $allowed[ $key ] ) ) {
$q->set( 'meta_key', $key );
$q->set( 'orderby', 'meta_value_num' );
$q->set( 'order', $allowed[ $key ] );
} else {
$q->set( 'orderby', 'date' );
}
$meta = [ 'relation' => 'AND' ];
if ( isset( $_GET['rent_max'] ) && $_GET['rent_max'] !== '' ) {
$meta[] = [
'key' => 'rent',
'value' => absint( $_GET['rent_max'] ),
'compare' => '<=',
'type' => 'NUMERIC',
];
}
if ( count( $meta ) > 1 ) {
$q->set( 'meta_query', $meta );
}
} );
$q->set( 'orderby', $_GET['sort'] ) のように、受け取った値をそのまま渡す実装です。orderby はSQLの構築に関わるため、許可リストで受ける形以外は書かないでください。
カスタムフィールドの保存では、nonce検証 → 権限確認 → サニタイズ → 保存の順を必ず守ります。1つでも欠けると、第三者に管理画面のURLを踏ませるだけで値を書き換えられます。
add_action( 'save_post_property', function ( $post_id ) {
// 1. 自動保存は対象外
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
// 2. nonce の検証
if ( ! isset( $_POST['estate_nonce'] )
|| ! wp_verify_nonce( sanitize_key( $_POST['estate_nonce'] ), 'estate_save' ) ) {
return;
}
// 3. この投稿を編集する権限があるか
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
// 4. 型に応じてサニタイズしてから保存
if ( isset( $_POST['rent'] ) ) {
update_post_meta( $post_id, 'rent', absint( $_POST['rent'] ) );
}
if ( isset( $_POST['station'] ) ) {
update_post_meta( $post_id, 'station',
sanitize_text_field( wp_unslash( $_POST['station'] ) ) );
}
} );
出力先の文脈に合わせて関数を選びます。「とりあえず esc_html」では属性値やURLで不足します。
| 出力先 | 関数 | 例 |
|---|---|---|
| 本文テキスト | esc_html() | <p><?php echo esc_html( $name ); ?></p> |
| 属性値 | esc_attr() | title="<?php echo esc_attr( $t ); ?>" |
| URL | esc_url() | href="<?php echo esc_url( $u ); ?>" |
| HTMLを許す本文 | wp_kses_post() | echo wp_kses_post( $body ); |
| JSに埋める値 | wp_json_encode() | var d = <?php echo wp_json_encode( $d ); ?>; |
if ( ! defined( 'ABSPATH' ) ) { exit; } を置き、直接アクセスを止める$wpdb->prepare() を通す。文字列連結でクエリを組まない脆弱性の大半はプラグインの放置が原因です。自動更新を有効にするか、月次の更新作業を保守契約に含めるかを、納品前に必ず決めてください。「あとで考える」が最も危険です。
動きを入れるほど、速度に注意が必要になります。以下は最低限の対応です。
function estate_area_counts() {
$key = 'estate_area_counts';
$data = get_transient( $key );
if ( false === $data ) {
$data = /* 集計処理 */ [];
set_transient( $key, $data, 6 * HOUR_IN_SECONDS );
}
return $data;
}
// 物件を保存したらキャッシュを捨てる
add_action( 'save_post_property', function () {
delete_transient( 'estate_area_counts' );
} );
transform と opacity のみで組む。top / left / width は毎フレーム再レイアウトが走るrequestAnimationFrame でまとめる。scroll イベントで直接DOMを触らないIntersectionObserver で止めるloading="lazy" + WebP。ただしファーストビューの画像には付けない(LCPが遅くなる)prefers-reduced-motion に対応する。アクセシビリティ要件であり、実装コストはほぼゼロチェックすると進捗が動きます。担当欄は、その項目を誰が確認するかです。
この仕様書の内容についてのご質問、既存サイトの改修のご相談も承ります。他社が作ったWordPressサイトの引き継ぎもお受けしています。管理画面に入れる状態であれば、現状の構成を調査してご報告します。
この仕様書そのものも、読ませるための作りにしています。
| 実装 | 目的 |
|---|---|
| 追従目次(スクロールスパイ) IntersectionObserver | いま読んでいる節を左の目次で示します。長い文書は「どこを読んでいるか」が分からなくなると読み止められます。 |
| 読み進みバー | 画面上端の線が全体の進捗を示します。残量が見えるだけで読了率が変わります。 |
| コードのコピー Clipboard API | コード枠のボタンで全文をコピーできます。API が使えない環境では、その旨をボタン上に表示して黙って失敗させません。 |
| チェックリストの集計 | 18項目のチェック状況を数と帯で集計します。納品前の確認を、そのままこの画面で進められます。 |
| ファイル構成の段階表示 | 画面に入ると1行ずつ現れます。階層の理解を助けるための順序で並べています。 |