From 1706da8598f444897b820184849bd43fcb978d91 Mon Sep 17 00:00:00 2001 From: Marco Date: Sat, 25 Jul 2026 17:11:48 +0000 Subject: [PATCH] Add schema.org structured data, Vorkasse email notice, newsletter duplicate detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Organization (site-wide), Product (/todo-cards), BlogPosting (every /blog/[slug]) JSON-LD via new app/lib/structuredData.ts — no new Payload fields needed, derived from existing data. Verified locally by curling each page and checking the rendered script tag. - Order confirmation email gains the same "please transfer to this account, processed after payment received" notice the invoice PDF already had for Vorkasse orders — OrderConfirmationData's new isManualPayment flag is set explicitly by each caller (never derived from paymentMethodTitle, which already broke once this session after a payment-methods rename). CompanySettings gains bankName (existed on the backend, was missing from the frontend's type/usage). - Newsletter signup now detects an already-subscribed email (verified empirically: Brevo's doubleOptinConfirmation endpoint gives identical 201 responses for new vs. already-confirmed contacts) via a GET /v3/contacts/{email} pre-check, and shows a distinct message instead of silently resending the confirmation mail. Success message text centralized in useNewsletterSignup.ts instead of duplicated across 4 forms. - Bumped @einfach-produktiv/invoicing to the version with the unpaid-notice layout fix (full width, more top spacing — was squeezed into the narrow paid-badge column). Co-Authored-By: Claude Sonnet 5 --- README.md | 76 ++++++++++++- app/api/checkout/route.ts | 1 + app/api/newsletter/subscribe/route.ts | 2 +- app/blog/[slug]/page.tsx | 6 +- app/challenge/components/EmailCapture.tsx | 4 +- app/company-settings-preview/page.tsx | 1 + app/components/Newsletter.tsx | 6 +- app/components/NewsletterModal.tsx | 6 +- app/layout.tsx | 14 ++- app/lib/brevo.ts | 40 +++++-- app/lib/emailTemplates.ts | 33 ++++++ app/lib/payload.ts | 1 + app/lib/structuredData.ts | 102 ++++++++++++++++++ app/lib/useNewsletterSignup.ts | 11 +- .../components/WeeklyImpulsesHero.tsx | 6 +- app/todo-cards/page.tsx | 13 ++- package-lock.json | 4 +- 17 files changed, 295 insertions(+), 31 deletions(-) create mode 100644 app/lib/structuredData.ts diff --git a/README.md b/README.md index 1bd50f6..2decacf 100644 --- a/README.md +++ b/README.md @@ -511,6 +511,24 @@ catalog, since an order only ever snapshots a numeric product id). touched spots and why some read a live setting and others a persisted per-order snapshot. +**Vorkasse instruction in the confirmation email (2026-07-25)** — the +invoice PDF already showed this (see `@einfach-produktiv/invoicing`'s own +`unpaidNoticeText`), but a customer often only glances at the email body +itself, not the attached PDF. `OrderConfirmationData` gained a required +`isManualPayment: boolean` field — **explicitly set by each caller** +(checkout route's manual branch: `true`; the Stripe webhook path: +always `false`, since only a *paid* Stripe order ever reaches that send at +all), deliberately **not** derived from `paymentMethodTitle` inside +`emailTemplates.ts` itself — that string ("Online-Zahlung", "Kreditkarte", +"Überweisung (Vorkasse)", ...) is exactly the kind of thing a +payment-methods rename already broke once this session (see +`isPaidImmediately()` in the invoicing package). When `isManualPayment` is +true, `vorkasseNotice()` renders a full-width block (own row, `margin-top: +20px` — not squeezed into the Gesamtsumme table) naming the bank details +(`CompanySettings.bankName`/`iban`/`bic`, now also exposed on the +frontend's own `CompanySettings` type — previously only `iban`/`bic` were) +and the "processed within 1–2 business days after payment received" note. + ### Checkout state persistence `app/lib/checkoutDraft.ts` — `localStorage` under `ep_checkout_draft`, @@ -1148,12 +1166,30 @@ unsynced. 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. +- **Already-subscribed detection (2026-07-25)** — `doubleOptinConfirmation` + itself gives no way to tell a brand-new signup apart from an + already-confirmed contact re-submitting the form (verified directly: + calling it twice for the same confirmed contact returns the identical + `201` both times, just silently resends the confirmation mail). So + `upsertNewsletterContact()` checks first via `GET /v3/contacts/{email}` + — a contact's `listIds` on Brevo is only populated once double opt-in + actually confirms, never for a merely-requested one, so its presence is + a reliable signal. If already subscribed: skips the resend entirely and + returns `{ ok: true, alreadySubscribed: true }` instead, which the UI + (`useNewsletterSignup.ts`) surfaces as a different message ("Diese + E-Mail-Adresse ist schon für unseren Newsletter angemeldet.") instead of + the normal "check your inbox" copy. The check fails open (any error → + proceed to the normal signup flow) — it's a UX nicety, never a reason to + block a real signup. - **`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 own fields). Each form keeps its own markup/visual style (`Newsletter`'s panel layout, `/challenge`'s hardcoded-hex-color palette, etc.) — only - the logic is shared, not a one-size-fits-all component. + the logic is shared, not a one-size-fits-all component. The success + message text itself also now lives here (`successMessage`), not + hardcoded 4 times per form — one of two variants depending on + `alreadySubscribed`. - **`app/lib/email.ts`** — `isValidEmail()`/`validateEmailFormat()`, the single plain-email-format check shared by every newsletter form *and* checkout's own email field (previously duplicated between @@ -1724,6 +1760,44 @@ breakpoint no longer needed to be as conservative as originally set: 768px. Both pushed from `md:flex-row` to `lg:flex-row` — stacked through the whole Tablet range, side by side again once there's real room. +## Structured data (schema.org, 2026-07-25) + +JSON-LD (`