【Next.js】ニュースレター登録フォームの作り方を送信状態とスパム対策まで解説
公開日: 2026年10月8日 · 読了目安 9 分
Next.js のサイトに置くニュースレター登録フォームの作り方です。置き場所と文言、サーバーアクションでの形式チェック、useActionState による送信中・成功・失敗の表示、確認メールとプライバシー表記、ハニーポットのスパム対策、ボタンのホバーアニメーションまでコード付きで解説します。
ニュースレター登録フォームはメール欄1つとボタン1つで作る
Next.js でブログやサービスのサイトを作っていて、読者にメールで更新を届けたくなった。
ところが登録フォームを置いても、思ったほど登録が増えない。入力欄が多いか、何が届くのかが伝わっていないことがよくあります。
答えは次のとおりです。フォームはメール欄1つとボタン1つにし、すぐ上に「何が・どのくらいの頻度で届くか」を書きます。
送信はサーバーアクション(サーバーで動く関数をフォームから直接呼ぶ仕組み)で受けます。送信中・成功・失敗の表示は、React 19 の useActionState で出し分けます。
この記事で分かることは次のとおりです。
登録フォームの置き場所と、中身が伝わる文言の書き方
メールの形式チェックとスパム対策をしたサーバーアクション
送信中・成功・失敗を出し分けるフォームの部品
確認メール(ダブルオプトイン)とプライバシー表記、ボタンのホバーアニメーション
ニュースレター登録フォームを短くする理由
登録フォームの役目は、読み終えた人の「また読みたい」をその場で受け取ることです。短くする理由は3つあります。
1つ目は、入力が1つ増えるごとに手が止まるからです。名前や会社名は、登録後のアンケートで聞けば足ります。
2つ目は、メールアドレスは渡すのに勇気がいる情報だからです。何が届くか分からないフォームには、読者は入力しません。
3つ目は、スマホで打ちやすいからです。メール欄が1つなら、端末の自動入力の候補から選ぶだけで済みます。
ニュースレター登録フォームの置き場所と文言を決める
コードを書く前に、どこに置いて何と書くかを決めます。ここが決まると、フォームの見た目もほぼ決まります。
置き場所は記事の最後とフッター
置き場所の基本は、記事を読み終えた位置とフッターです。内容に満足したところで、次の更新を受け取る理由が生まれます。
開いた直後に画面を覆うポップアップは避けます。まだ何も読んでいない人に登録を頼んでも、閉じられるだけです。
サービスの LP なら、最後の申し込みボタンの近くに小さく置きます。CTA セクションの作り方は https://design-layer.com/ja/articles/nextjs-cta-section-guide で解説しています。
見出しには「何が・どのくらいの頻度で」を書く
「ニュースレターに登録」だけでは、何が届くか想像できません。見出しと1行の説明で、中身と頻度を具体的に書きます。
見出し:週に1通、Next.js の小さな実装例を届けます
説明:毎週木曜の朝に配信。いつでも1クリックで停止できます
ボタン:登録する(「送信」より、押した結果が分かる言葉にする)
頻度は守れる数字にします。「毎週」と書いて月1回しか届かないと、読者の信頼を失います。
Step1:ニュースレター登録を受けるサーバーアクションを書く
まず、送られたメールアドレスを受け取る関数を書きます。ファイルの先頭に "use server" と書くと、その関数はサーバーだけで動きます。
ブラウザの入力チェックは、自動送信などで簡単にすり抜けられます。そのため、サーバー側でも必ず形式を確かめます。
登録済みのアドレスが送られても、同じ成功メッセージを返します。「登録済みです」と返すと、他人のアドレスが登録されているか調べられてしまうからです。
"use server";
import { addSubscriber } from "@/lib/newsletter";
export type SubscribeState = {
status: "idle" | "success" | "error";
message: string;
email?: string;
};
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const SUCCESS_MESSAGE =
"確認メールを送りました。メール内のリンクを押すと登録が完了します。";
export async function subscribe(
_prevState: SubscribeState,
formData: FormData,
): Promise<SubscribeState> {
// ハニーポットに値があればボット。登録せず、成功と同じ返事をする
if (formData.get("hp_field")) {
return { status: "success", message: SUCCESS_MESSAGE };
}
const email = String(formData.get("email") ?? "").trim();
if (email.length > 254 || !EMAIL_PATTERN.test(email)) {
return {
status: "error",
message: "メールアドレスの形式を確かめてください。",
email,
};
}
try {
await addSubscriber(email);
} catch (error) {
console.error(error);
return {
status: "error",
message: "登録できませんでした。少し時間をおいて、もう一度お試しください。",
email,
};
}
return { status: "success", message: SUCCESS_MESSAGE };
}ハニーポットでボットの登録を防ぐ
ハニーポットは、人には見えない入力欄を1つ置くスパム対策です。全部の欄を自動で埋めるボットだけが、ここに値を入れます。
欄の名前は、ブラウザの自動入力が埋めない名前にします。company などにすると、読者の入力にも値が入るおそれがあります。
値が入っていたら、登録せずに成功と同じ返事を返します。失敗と返すと、ボットに見抜く手がかりを与えてしまいます。
それでも大量の送信が続くなら、IP アドレスごとの回数制限や CAPTCHA(人かどうかを確かめる仕組み)を足してください。
配信サービスへの登録は1つの関数にまとめる
実際の登録は、使っているメール配信サービスの API に送ります。URL や必要な項目はサービスごとに違うので、公式ドキュメントに合わせて書き換えてください。
呼び出しを1つの関数にまとめておくと、サービスを乗り換えるときに直す場所が1か所で済みます。登録済みのときにエラーを返すサービスなら、ここで成功として扱う分岐を足します。
API キーは環境変数に置き、名前を NEXT_PUBLIC_ で始めないでください。その名前にすると、ブラウザに届くコードに値が含まれます。
// 使っている配信サービスの API に合わせて、URL・ヘッダー・本文を書き換える
export async function addSubscriber(email: string) {
const url = process.env.NEWSLETTER_API_URL;
const apiKey = process.env.NEWSLETTER_API_KEY;
if (!url || !apiKey) {
throw new Error("Newsletter API is not configured");
}
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + apiKey,
},
body: JSON.stringify({ email }),
});
if (!res.ok) {
throw new Error("Newsletter API error: " + res.status);
}
}Step2:useActionState でニュースレター登録の状態を表示する
次に、フォームの部品を作ります。送信の状態を画面で扱うので、ファイルの先頭にブラウザで動かす印("use client")を書きます。
useActionState は、送信結果と送信中かどうかを返す React 19 の仕組みです。Step1 の関数を渡すと、返した状態がそのまま画面に届きます。
"use client";
import Link from "next/link";
import { useActionState } from "react";
import { subscribe, type SubscribeState } from "@/app/actions/subscribe";
import { SubscribeButton } from "@/components/SubscribeButton";
const initialState: SubscribeState = { status: "idle", message: "" };
export function NewsletterForm() {
const [state, formAction, isPending] = useActionState(subscribe, initialState);
const hasError = state.status === "error";
return (
<section className="mx-auto max-w-xl px-6 py-20">
<h2 className="text-2xl font-semibold">週に1通、Next.js の小さな実装例を届けます</h2>
<p className="mt-2 text-zinc-600">毎週木曜の朝に配信。いつでも1クリックで停止できます。</p>
{state.status === "success" ? (
<p role="status" className="mt-6 rounded-xl bg-emerald-50 p-4 text-sm text-emerald-800">
{state.message}
</p>
) : (
<form action={formAction} className="relative mt-6">
<label htmlFor="newsletter-email" className="text-sm font-medium">
メールアドレス
</label>
<div className="mt-2 flex flex-col gap-3 sm:flex-row">
<input
id="newsletter-email"
name="email"
type="email"
required
autoComplete="email"
placeholder="you@example.com"
defaultValue={state.email}
aria-invalid={hasError}
aria-describedby={hasError ? "newsletter-error" : undefined}
className="min-h-11 flex-1 rounded-full border border-zinc-300 px-5 text-base outline-none focus:border-zinc-900"
/>
<SubscribeButton pending={isPending} />
</div>
{/* ハニーポット:人には見えない欄。ボットだけが値を入れる */}
<div aria-hidden="true" className="absolute -left-[9999px] h-px w-px overflow-hidden">
<label htmlFor="newsletter-hp">この欄は空のままにしてください</label>
<input id="newsletter-hp" name="hp_field" type="text" tabIndex={-1} autoComplete="off" />
</div>
{hasError && (
<p id="newsletter-error" role="alert" className="mt-2 text-sm text-red-600">
{state.message}
</p>
)}
<p className="mt-3 text-xs text-zinc-500">
登録したアドレスは配信にだけ使います。{" "}
<Link href="/privacy" className="underline underline-offset-2">
プライバシーポリシー
</Link>
</p>
</form>
)}
</section>
);
}コードのポイント
type="email" と required で、送る前にブラウザが形式を確かめる
autoComplete="email" で、スマホの自動入力の候補が出る
エラー文は aria-describedby で入力欄と結び、読み上げでも伝わる
成功したらフォームを消し、role="status" の文で結果を知らせる
送信後はフォームが空に戻るので、失敗したときは入力したアドレスを defaultValue で戻す
ラベルは placeholder で代用せず、入力欄の上に見える形で置きます。打ち始めると placeholder は消え、何の欄か分からなくなるからです。
送信中はボタンを押せなくする
送信中にもう一度押されると、同じ登録が2回送られます。isPending が true の間はボタンを無効にし、文言を「送信中…」に変えます。
この切り替えは、次の Step3 のボタンの部品に入れています。
Step3:ニュースレター登録ボタンにホバーアニメーションを付ける
最後に、ボタンにホバーの動きを足します。文字が上に抜け、下から同じ文字が入ってくる動きです。
Motion(旧 Framer Motion)の variants を使うと、ボタンに乗せた状態を中の文字にも伝えられます。Motion の基本は https://design-layer.com/ja/articles/nextjs-framer-motion-guide で解説しています。
"use client";
import { motion, useReducedMotion, type Variants } from "motion/react";
const rollOut: Variants = { rest: { y: "0%" }, hover: { y: "-100%" } };
const rollIn: Variants = { rest: { y: "100%" }, hover: { y: "0%" } };
const transition = { duration: 0.25, ease: "easeOut" } as const;
export function SubscribeButton({ pending }: { pending: boolean }) {
const reduceMotion = useReducedMotion();
const label = pending ? "送信中…" : "登録する";
const canAnimate = !reduceMotion && !pending;
return (
<motion.button
type="submit"
disabled={pending}
initial="rest"
animate="rest"
whileHover={canAnimate ? "hover" : undefined}
whileFocus={canAnimate ? "hover" : undefined}
className="inline-flex min-h-11 items-center justify-center rounded-full bg-zinc-900 px-6 text-sm font-medium text-white disabled:cursor-not-allowed disabled:opacity-60"
>
<span className="relative block overflow-hidden">
<motion.span variants={rollOut} transition={transition} className="block">
{label}
</motion.span>
<motion.span
aria-hidden="true"
variants={rollIn}
transition={transition}
className="absolute inset-0 block"
>
{label}
</motion.span>
</span>
</motion.button>
);
}動きを減らす設定ではホバーを止める
OS で動きを減らす設定にしている人には、useReducedMotion が true を返します。そのときは whileHover を渡さず、文字を動かしません。
下から入ってくる文字には aria-hidden を付けます。付けないと、ボタンの名前が2回読み上げられます。
ニュースレター登録の確認メールとプライバシー表記
ボタンを押しただけで配信を始めると、打ち間違えたアドレスや他人のアドレスにもメールが届きます。
そこで、配信サービスのダブルオプトインの設定を使います。登録後に確認メールを送り、本文のリンクが押されたら登録を完了する方式です。
この方式にする場合、成功の文言は「登録しました」ではなく「確認メールを送りました」にします。迷惑メールフォルダも確かめるよう一言添えると親切です。
フォームの下には、プライバシーポリシーへのリンクを1行置きます。アドレスを何に使うかと、いつでも配信を止められることを書きます。
日本で広告や宣伝のメールを送る場合は、特定電子メール法の対象になります。事前の同意を得て、その記録を残します。
メールには、送信者の名前や連絡先と、配信停止の方法を表示します。
海外の読者がいる場合は、その国の法律も確認してください。
ニュースレター登録フォームでよくある失敗と公開前チェック
名前・会社名・職種まで聞く。最初はメール欄1つにし、ほかは登録後に聞く
ラベルを placeholder で代用する。打ち始めると何の欄か分からなくなる
ブラウザのチェックだけで済ませる。サーバー側でも形式を確かめる
成功しても何も表示されない。確認メールを開くなど、次にすることまで書く
ボタンが小さい。スマホで押しやすいよう、高さを 44px 以上にする
page.tsx 全体に "use client" を書く。フォームの部品だけに切り出す
公開前に、自分のアドレスで実際に登録してみてください。確認メールの届き方と、配信停止のリンクが動くかまで確かめます。
問い合わせフォームも、同じサーバーアクションの形で作れます。作り方は https://design-layer.com/ja/articles/nextjs-contact-section-guide で解説しています。
完成したニュースレター登録フォームをコピーして使う方法

ボタンの文字が立体的に回る動きや、成功後の表示まで作り込むと、調整に時間がかかります。
その場合は、完成したセクションをブラウザで動かして確かめてから、自分の Next.js に載せる方法もあります。
DesignLayer の「フリップCTAニュースレター」は、ホバーでボタンの文字が 3D に反転する登録フォームです。メールの形式チェックと、登録後の成功表示も入っています。
詳細ページの「Cursor 用にコピー」を押すと、組み込みの指示・ライブプレビューの URL・本体の TSX・npm の依存がまとめてコピーされます。Cursor や Claude に貼れば、自分のプロジェクトに組み込めます。
閲覧とライブプレビューは無料で、アカウントもカードも要りません。無料のソースコピーは日本時間で1日3回までです。
カタログは https://design-layer.com/ja/components から開けます。載せたあとは、送信の処理を Step1 のサーバーアクションにつなぐと、実際の登録まで動きます。
ニュースレター登録フォームについてよくある質問
最初はメール欄だけで十分です。名前で呼びかけたい場合も、登録後のメールで任意に聞けます。
まとめ:Next.js でニュースレター登録フォームを作る手順
置き場所は記事の最後とフッター。開いた直後のポップアップは避ける
フォームはメール欄1つとボタン1つ。見出しに中身と頻度を書く
サーバーアクションで形式を確かめ、ハニーポットでボットを弾く
useActionState で送信中・成功・失敗を出し分ける
確認メールとプライバシーの1行を添え、ボタンには控えめなホバーの動きを付ける
まずは Step1 と Step2 のコードで、自分のアドレスを登録してみてください。ボタンをカーソルに引き寄せる動きは https://design-layer.com/ja/articles/nextjs-magnetic-button-guide で解説しています。