Add /newsletter-confirmed double-opt-in confirmation page
Brevo's redirectionUrl now points here instead of the homepage — a static page matching /bestellbestaetigung's visual language (brand- tinted checkmark circle, serif display heading, thin brand divider). No query params to read; Brevo's confirmation redirect carries nothing this page needs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -56,7 +56,7 @@ independent, see the "Monitoring & alerting" section). `STRIPE_SECRET_KEY`,
|
||||
(Stripe)" below. `BREVO_API_KEY`, `BREVO_LIST_ID`,
|
||||
`BREVO_DOUBLE_OPTIN_TEMPLATE_ID` (no safe default — required for
|
||||
newsletter signups to trigger a confirmation email at all),
|
||||
`BREVO_DOI_REDIRECT_URL` (optional, defaults to the homepage) — see
|
||||
`BREVO_DOI_REDIRECT_URL` (optional, defaults to `/newsletter-confirmed`) — see
|
||||
"Newsletter signup" below. Set in Coolify's app settings for production,
|
||||
not in a committed `.env`.
|
||||
|
||||
@@ -1074,9 +1074,13 @@ unsynced.
|
||||
(`POST /v3/contacts/attributes/normal/OPT_IN_SOURCE`) or Brevo silently
|
||||
drops it on every request (no error at all, just never stored) rather
|
||||
than rejecting the request. `redirectionUrl` (where Brevo sends the
|
||||
contact after they click confirm) defaults to the homepage via
|
||||
`BREVO_DOI_REDIRECT_URL` — no dedicated "danke, bestätigt" landing page
|
||||
exists yet.
|
||||
contact after they click confirm) defaults to `/newsletter-confirmed`
|
||||
via `BREVO_DOI_REDIRECT_URL` — a static confirmation page
|
||||
(`app/newsletter-confirmed/page.tsx`), same visual language as
|
||||
`/bestellbestaetigung` (brand-tinted circular checkmark, serif display
|
||||
heading, thin brand divider). No query params to read — Brevo's
|
||||
redirect carries nothing this page needs, unlike `/checkout/verarbeitung`
|
||||
which polls actual payment status.
|
||||
- **`app/lib/useNewsletterSignup.ts`** — the shared email/consent/submit
|
||||
state + on-blur validation + refocus-on-invalid-submit behind all four
|
||||
forms (same "state of the art, simple" input-quality bar as checkout's
|
||||
|
||||
+1
-4
@@ -33,10 +33,7 @@ export async function upsertNewsletterContact(
|
||||
if (!apiKey || !listId || !templateId) {
|
||||
return { ok: false, reason: "BREVO_API_KEY/BREVO_LIST_ID/BREVO_DOUBLE_OPTIN_TEMPLATE_ID nicht konfiguriert." };
|
||||
}
|
||||
// No dedicated "danke, bestätigt" landing page exists yet — falls back
|
||||
// to the homepage rather than blocking double opt-in on that page
|
||||
// existing first. Revisit once one exists.
|
||||
const redirectionUrl = process.env.BREVO_DOI_REDIRECT_URL || "https://einfach-produktiv.mk360.de/";
|
||||
const redirectionUrl = process.env.BREVO_DOI_REDIRECT_URL || "https://einfach-produktiv.mk360.de/newsletter-confirmed";
|
||||
|
||||
try {
|
||||
const res = await fetch(BREVO_DOUBLE_OPTIN_URL, {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { Reveal } from "../components/Reveal";
|
||||
import { Footer } from "../components/Footer";
|
||||
import { TrustRow } from "../components/TrustRow";
|
||||
|
||||
// robots: noindex — transactional landing page (Brevo's double opt-in
|
||||
// redirectionUrl target, see app/lib/brevo.ts's BREVO_DOI_REDIRECT_URL),
|
||||
// same reasoning as /bestellbestaetigung and /checkout: nothing here is
|
||||
// meant to be found via search, only reached via the confirmation link.
|
||||
export const metadata: Metadata = {
|
||||
title: "Newsletter bestätigt",
|
||||
description: "Deine Newsletter-Anmeldung bei einfach produktiv ist bestätigt.",
|
||||
robots: {
|
||||
index: false,
|
||||
follow: true,
|
||||
},
|
||||
};
|
||||
|
||||
// Static — Brevo's confirmation click lands here with no query params to
|
||||
// read, so unlike /bestellbestaetigung (which hydrates a sessionStorage
|
||||
// order snapshot) or /checkout/verarbeitung (which polls payment status),
|
||||
// this page has nothing to fetch or wait on. Same visual language as
|
||||
// those two: warm bg-bg-base, brand-tinted circular icon, serif display
|
||||
// heading, thin brand divider — see BestellbestaetigungContent.tsx for
|
||||
// the pattern this mirrors.
|
||||
export default function NewsletterConfirmedPage() {
|
||||
return (
|
||||
<>
|
||||
<main className="flex flex-col flex-1 bg-bg-base">
|
||||
<Reveal className="flex flex-col gap-4 items-center text-center pt-24 pb-16 px-[var(--layout-padding-x)] w-full">
|
||||
<div className="flex items-center justify-center size-14 rounded-full bg-brand/10 text-brand shrink-0">
|
||||
<svg viewBox="0 0 24 24" className="size-6" fill="none" aria-hidden="true">
|
||||
<path d="M5 13.5 9.5 18 19 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<p
|
||||
className="font-semibold text-display text-text-primary"
|
||||
style={{ fontFamily: "var(--font-playfair)" }}
|
||||
>
|
||||
Bestätigt!
|
||||
</p>
|
||||
<p
|
||||
className="font-semibold text-h3 text-text-primary"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
>
|
||||
Du bist jetzt Teil unseres Newsletters.
|
||||
</p>
|
||||
<div className="h-[0.125rem] w-8 bg-brand" />
|
||||
<p className="text-body text-text-muted max-w-[28rem] pt-2">
|
||||
Schön, dass du dabei bist! Ab jetzt bekommst du hin und wieder Impulse, neue Produkte
|
||||
und kleine Erinnerungen von uns, damit dein Alltag ein bisschen leichter wird.
|
||||
</p>
|
||||
<Link
|
||||
href="/shop"
|
||||
className="inline-flex items-center justify-center py-4 px-8 mt-4 rounded-sm bg-brand hover:bg-brand-hover active:scale-[0.97] transition-all font-bold text-body text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2 focus-visible:ring-offset-bg-base"
|
||||
>
|
||||
Jetzt stöbern
|
||||
</Link>
|
||||
</Reveal>
|
||||
<TrustRow />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user