【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" と書くと、その関数はサーバーだけで動きます。

ブラウザの入力チェックは、自動送信などで簡単にすり抜けられます。そのため、サーバー側でも必ず形式を確かめます。

登録済みのアドレスが送られても、同じ成功メッセージを返します。「登録済みです」と返すと、他人のアドレスが登録されているか調べられてしまうからです。

app/actions/subscribe.ts
"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_ で始めないでください。その名前にすると、ブラウザに届くコードに値が含まれます。

lib/newsletter.ts
// 使っている配信サービスの 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 の関数を渡すと、返した状態がそのまま画面に届きます。

components/NewsletterForm.tsx
"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 で解説しています。

components/SubscribeButton.tsx
"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 で解説しています。

完成したニュースレター登録フォームをコピーして使う方法

DesignLayer の「フリップCTAニュースレター」の詳細ページ。左でライブプレビューを確かめ、右の「Cursor 用にコピー」でソースをコピーできる
DesignLayer の「フリップCTAニュースレター」の詳細ページ。左でライブプレビューを確かめ、右の「Cursor 用にコピー」でソースをコピーできる

ボタンの文字が立体的に回る動きや、成功後の表示まで作り込むと、調整に時間がかかります。

その場合は、完成したセクションをブラウザで動かして確かめてから、自分の 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 で解説しています。

【Next.js】ニュースレター登録フォームの作り方を送信状態とスパム対策まで解説 | DesignLayer