「ECサイトを構築するまでの予算・規模はないけど、フォームだけで簡易注文を受けられるようにしたい!」
といったクライアントの要望って、実務で結構ありませんか?
私が実際経験した内容としては、
- 注文フォームをつけたいけど、ECサイトを構築するほどではない商品規模
- サイト側に決済機能をつけるほどではない規模
- 送料は一律設定で問題ない
- 注文商品や価格は変動するのでお客様自身で更新したい
- プラグインのサブスク契約(例えばACF PRO)などをクライアントに負担させる規模でもない
という要望だったので、極力シンプルな設計でそれを実現する方法を考えました。
今回はContactForm7とWordPress標準のカスタム投稿タイプだけで、簡易注文フォームを実装する方法をご紹介します。
- カスタム投稿タイプで商品を管理(商品名・金額をシンプルに登録)
- ContactForm7 のフォーム上に商品プルダウンを動的生成
- 複数商品・数量指定に対応、金額をリアルタイム合算表示
- 送信メールに注文内容を整形して届ける
目次 非表示
完成イメージ

- 商品プルダウン + 数量セレクト が1セット表示される
- 「他の商品を追加する」ボタンで行を追加できる
- 合計金額がリアルタイムで表示される
- 送信ボタンを押すと管理者メールに注文内容が整形されて届く
決済システムはついていないので、注文フォーム送信後の返信メールで、銀行振込連絡先やURL型決済リンクを送らせるアナログ方式で運用することになります。
販売規模が小さい、手動で管理する、というお客様向きです。
前提・準備
- WordPress が動いていること
- ContactForm 7(CF7) がインストール済みであること
- 子テーマを使用していること(functions.php の編集が必要なため)
- 商品を設置する固定ページが用意されていること
手順
STEP 1:カスタム投稿タイプを登録する
まずは、子テーマの functions.php に以下を追記します。
php
// カスタム投稿タイプ「商品」を登録
function pdo_register_products_cpt() {
$args = [
'label' => '商品',
'labels' => [
'name' => '商品',
'singular_name' => '商品',
'add_new_item' => '商品を追加',
'edit_item' => '商品を編集',
'new_item' => '新しい商品',
'all_items' => 'すべての商品',
],
'public' => false, // フロントに単独ページは不要
'show_ui' => true, // 管理画面には表示する
'show_in_menu' => true,
'supports' => ['title', 'editor'], // タイトル(商品名)と本文(金額)だけ使う
'menu_icon' => 'dashicons-cart',
];
register_post_type('pdo_product', $args);
}
add_action('init', 'pdo_register_products_cpt');
// 商品投稿タイプのみクラシックエディタ(非ブロックエディタ)にする
add_filter('use_block_editor_for_post_type', function($use_block_editor, $post_type) {
if ($post_type === 'pdo_product') {
return false;
}
return $use_block_editor;
}, 10, 2);STEP 2:商品データを登録する
管理画面のサイドメニューに「商品」が追加されているので、ここから商品を登録します。

- タイトル → 商品名を入力(例:「Aセット」)
- 本文 → 金額を数値のみ入力(例:
7700)

STEP 3:固定ページテンプレートにPHP+JSを実装する
フォームを設置する固定ページ用のテンプレートファイルを子テーマに作成し、 以下のコードを wp_footer アクションの直前(フッター付近) に貼り付けます。
<?php
// 商品データをカスタム投稿タイプから取得する
$products = [];
$query = new WP_Query([
'post_type' => 'pdo_product',
'posts_per_page' => -1, // 全件取得
'post_status' => 'publish',
'orderby' => 'menu_order title',
'order' => 'ASC',
]);
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
$products[] = [
'name' => get_the_title(),
// 本文から余計なタグ・空白を除去して数値だけ取り出す
'price' => intval(trim(strip_tags(get_the_content()))),
];
}
wp_reset_postdata();
}
$products_json = json_encode($products, JSON_UNESCAPED_UNICODE);
?>
<script type="text/javascript">
const productData = <?php echo $products_json; ?>;
document.addEventListener("DOMContentLoaded", function () {
const productContainer = document.getElementById("product-container");
const totalPriceSpan = document.getElementById("total-price");
const addProductButton = document.getElementById("add-product");
const hiddenField = document.getElementById("product-details");
const formattedField = document.getElementById("formatted-product-details");
if (!productContainer || !totalPriceSpan || !addProductButton || !hiddenField || !formattedField) {
return;
}
const form = productContainer.closest("form");
if (!form) {
console.error("CF7 form not found.");
return;
}
const firstItem = productContainer.querySelector(".product-item");
let lastSelectedState = [];
function createDefaultOption() {
const option = document.createElement("option");
option.value = "";
option.textContent = "▼商品をリストから選択してください";
option.dataset.price = "0";
return option;
}
function populateProductOptions(selectElement) {
selectElement.innerHTML = "";
selectElement.appendChild(createDefaultOption());
productData.forEach((product, index) => {
const option = document.createElement("option");
option.value = String(index);
option.dataset.price = String(product.price);
option.textContent = product.name + "(" + Number(product.price).toLocaleString() + "円)";
selectElement.appendChild(option);
});
selectElement.selectedIndex = 0;
selectElement.value = "";
}
function calculateTotal() {
let total = 0;
productContainer.querySelectorAll(".product-item").forEach((item) => {
const select = item.querySelector(".product-select");
const quantitySelect = item.querySelector(".product-quantity");
if (!select || !quantitySelect) return;
if (select.value === "") return;
const selectedOption = select.options[select.selectedIndex];
const price = parseInt(selectedOption.dataset.price || "0", 10);
const quantity = parseInt(quantitySelect.value || "1", 10);
total += price * quantity;
});
totalPriceSpan.textContent = total.toLocaleString();
}
function clearProductError() {
const oldError = document.getElementById("product-select-error");
if (oldError) oldError.remove();
}
function showProductError(message) {
clearProductError();
const error = document.createElement("div");
error.id = "product-select-error";
error.style.color = "#dc3232";
error.style.fontSize = "14px";
error.style.marginTop = "8px";
error.style.marginBottom = "8px";
error.textContent = message;
const totalBox = form.querySelector(".total-price");
if (totalBox) {
totalBox.parentNode.insertBefore(error, totalBox);
} else {
productContainer.insertAdjacentElement("afterend", error);
}
}
function bindProductItemEvents(item, isFirstItem = false) {
const select = item.querySelector(".product-select");
const quantitySelect = item.querySelector(".product-quantity");
const removeButton = item.querySelector(".remove-product");
if (!select || !quantitySelect) return;
populateProductOptions(select);
quantitySelect.value = "1";
select.addEventListener("change", function () {
clearProductError();
calculateTotal();
});
quantitySelect.addEventListener("change", function () {
calculateTotal();
});
if (removeButton) {
if (isFirstItem) {
removeButton.style.display = "none";
} else {
removeButton.style.display = "inline-block";
removeButton.addEventListener("click", function () {
item.remove();
clearProductError();
calculateTotal();
});
}
}
}
function addProductRow() {
const newItem = firstItem.cloneNode(true);
bindProductItemEvents(newItem, false);
productContainer.appendChild(newItem);
}
function getCurrentState() {
const rows = [];
productContainer.querySelectorAll(".product-item").forEach((item) => {
const select = item.querySelector(".product-select");
const quantitySelect = item.querySelector(".product-quantity");
rows.push({
value: select ? select.value : "",
quantity: quantitySelect ? quantitySelect.value : "1"
});
});
return rows;
}
function restoreState(state) {
if (!Array.isArray(state) || state.length === 0) return;
productContainer.innerHTML = "";
state.forEach((row, index) => {
const item = firstItem.cloneNode(true);
bindProductItemEvents(item, index === 0);
const select = item.querySelector(".product-select");
const quantitySelect = item.querySelector(".product-quantity");
if (select) select.value = row.value ?? "";
if (quantitySelect) quantitySelect.value = row.quantity ?? "1";
productContainer.appendChild(item);
});
calculateTotal();
}
function resetProductUI() {
productContainer.innerHTML = "";
const newFirstItem = firstItem.cloneNode(true);
productContainer.appendChild(newFirstItem);
bindProductItemEvents(newFirstItem, true);
clearProductError();
hiddenField.value = "";
formattedField.value = "";
calculateTotal();
}
function collectSelectedProducts() {
const mergedProducts = {};
let totalAmount = 0;
productContainer.querySelectorAll(".product-item").forEach((item) => {
const select = item.querySelector(".product-select");
const quantitySelect = item.querySelector(".product-quantity");
if (!select || !quantitySelect) return;
if (select.value === "") return;
const index = parseInt(select.value, 10);
const quantity = parseInt(quantitySelect.value || "1", 10);
if (isNaN(index) || !productData[index]) return;
const product = productData[index];
const name = product.name;
const price = parseInt(product.price, 10) || 0;
if (!mergedProducts[index]) {
mergedProducts[index] = {
name: name,
price: price,
quantity: 0,
totalPrice: 0
};
}
mergedProducts[index].quantity += quantity;
mergedProducts[index].totalPrice += price * quantity;
totalAmount += price * quantity;
});
const productDetails = Object.values(mergedProducts);
let formattedText = "";
productDetails.forEach((product) => {
formattedText += "・" + product.name +
"(数量:" + product.quantity +
"/単価:" + product.price.toLocaleString() +
"円)= " + product.totalPrice.toLocaleString() + "円\n";
});
return {
productDetails,
formattedText,
totalAmount
};
}
function validateProducts() {
clearProductError();
const result = collectSelectedProducts();
if (result.productDetails.length === 0) {
showProductError("商品を1つ以上選択してください。");
alert("商品を1つ以上選択してください。");
return false;
}
hiddenField.value = JSON.stringify(result.productDetails);
formattedField.value = result.formattedText + "\n合計見積もり金額:" + result.totalAmount.toLocaleString() + "円";
return true;
}
bindProductItemEvents(firstItem, true);
calculateTotal();
addProductButton.addEventListener("click", function () {
addProductRow();
});
form.addEventListener("click", function (event) {
const submitButton = event.target.closest(".wpcf7-submit");
if (!submitButton) return;
lastSelectedState = getCurrentState();
if (!validateProducts()) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
return false;
}
}, true);
form.addEventListener("submit", function (event) {
lastSelectedState = getCurrentState();
if (!validateProducts()) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
return false;
}
}, true);
document.addEventListener("wpcf7invalid", function (event) {
if (event.target !== form) return;
setTimeout(function () {
restoreState(lastSelectedState);
}, 50);
});
document.addEventListener("wpcf7mailfailed", function (event) {
if (event.target !== form) return;
setTimeout(function () {
restoreState(lastSelectedState);
}, 50);
});
document.addEventListener("wpcf7spam", function (event) {
if (event.target !== form) return;
setTimeout(function () {
restoreState(lastSelectedState);
}, 50);
});
document.addEventListener("wpcf7mailsent", function (event) {
if (event.target !== form) return;
resetProductUI();
});
});
</script>STEP 4:CF7 のフォームを設定する
CF7の管理画面でフォームを編集し、冒頭に以下の hidden タグを追加します。
[hidden product-details id:product-details]
[hidden formatted-product-details id:formatted-product-details]次に、フォームに商品選択UIを挿入します。フォームの任意の位置に貼り付けてください。
<div id="product-container"><div class="product-item"><select class="product-select"><option value="" data-price="0" selected disabled>▼商品をリストから選択してください</option></select>数量:<select class="product-quantity"><option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option><option value="6">6</option><option value="7">7</option><option value="8">8</option><option value="9">9</option><option value="10">10</option></select><button type="button" class="remove-product">削除</button></div></div>
<button type="button" id="add-product">他の商品を追加する</button>
<div class="total-price">合計金額:<span id="total-price">0</span> 円</div>STEP 5:CSSを追加する
管理画面 → 外観 → カスタマイズ → 追加CSS に以下を貼り付けてください。
あくまで下記はサンプルなので、デザインはサイトに合わせて変更してくださいね!
/* =====================
簡易注文フォーム
===================== */
#product-container {
margin-bottom: 1em;
}
.product-item {
margin-top: 1em;
padding-top: 1em;
border-top: 1px dashed #ddd;
}
/* CF7が<p>と<br>を自動挿入するため、<p>をflexコンテナにする */
.product-item p {
display: flex;
align-items: center;
flex-wrap: nowrap;
gap: 10px;
margin: 0;
font-size: 1rem;
}
/* CF7が挿入する<br>を非表示にする */
.product-item p br {
display: none;
}
.product-select {
flex: 0 1 auto;
min-width: 0;
max-width: 320px;
width: auto !important;
padding: 0.5em;
box-sizing: border-box;
}
.product-quantity {
flex: 0 0 auto;
width: auto !important;
padding: 0.5em;
margin-left: 0.5em;
}
.remove-product {
padding: 0.5em 1em;
background-color: #f44336;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
}
.remove-product:hover {
background-color: #d32f2f;
}
#add-product {
display: inline-block;
margin-top: 1em;
padding: 0.5em 1.2em;
background-color: #4caf50;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
}
#add-product:hover {
background-color: #45a049;
}
.total-price {
margin-top: 1.5em;
font-size: 1.6em;
font-weight: bold;
}
/* エラー時 */
.product-select.error {
border: 2px solid red;
background-color: #ffe6e6;
}
/* スマホ対応 */
@media (max-width: 600px) {
.product-item p {
flex-direction: column;
align-items: stretch;
}
.product-select,
.product-quantity,
.remove-product {
width: 100% !important;
}
.remove-product {
text-align: center;
margin-top: 5px;
}
}STEP 6:メール設定をする
CF7 の「メール」タブを開き、メール本文の適切な場所に以下を追記します。
■ご希望商品:
[formatted-product-details]送信されるメールのイメージ:
■ご希望商品:
・商品Aセット(数量:1/単価:7,700円)= 7,700円
・商品Bセット(数量:2/単価:4,950円)= 9,900円
合計見積もり金額:17,600円よくあるトラブルと対処
商品がプルダウンに表示されない
PHPの WP_Query でカスタム投稿タイプ名(pdo_product)を変更している場合、 functions.php と テンプレートの両方で名前が一致しているか確認してください。
また、商品が「下書き」状態になっていないか確認してください。post_status => 'publish' で取得しているため、公開済みのものだけが対象です。
金額が0円になる
商品登録画面(カスタムタイプ投稿)の本文欄に数値以外の文字(スペース・改行・HTMLタグ)が含まれている可能性があります。コピー&ペーストをしてくると、不要なタグが混入することがあります。
管理画面の商品編集画面で、本文をテキストモードに切り替えて数値のみになっているか確認してください。
確認画面と組み合わせたい
以前の記事(ContactForm7に確認画面をつける方法)で紹介した確認モーダルと組み合わせることができます。
ただし、確認モーダルの FIELD_LABELS_MAP に formatted-product-details の表示設定を追加するなど、 若干の調整が必要ですので、ご自身でカスタマイズができない方に向けては、また追ってブログでご紹介できたらなと思います。
お急ぎの方は、パリスタデザイン事務所までご相談ください(見積もりは無料です)。
まとめ
- ACF Pro を使わず、WordPress標準のカスタム投稿タイプだけで商品管理できる
- 商品の追加・削除・金額変更は管理画面から行えるので、クライアントが直接操作可能
- JS と PHP のコードをコピペするだけで動く軽量な実装
商品点数が増えても管理画面から操作するだけなので、納品後のクライアント運用にも向いています。
ぜひ試してみてください!

WordPressまたはContactForm7のアップデートで動作しなくなる可能性があります。
お困りのことがあればコメントでお知らせください。
この記事を書いた人

パリスタデザイン事務所のWebデザイナー・フロントエンドエンジニア。
WordPress構築・カスタマイズを中心に、CMS設計・構築や情報設計を得意としています。
AIやWeb技術、PC/ガジェットを活用しながら、「無理なく続く仕組みづくり」や実務に役立つ知見を発信しています。

