Add schema.org structured data, Vorkasse email notice, newsletter duplicate detection

- 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 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-25 17:11:48 +00:00
parent 1dbc0c31ff
commit 1706da8598
17 changed files with 295 additions and 31 deletions
+33
View File
@@ -66,6 +66,29 @@ function escapeHtml(s: string): string {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
// Vorkasse (Überweisung/manual) instruction — the invoice PDF already
// shows this same information (see @einfach-produktiv/invoicing's own
// unpaidNoticeText), but a customer often only glances at the email body
// itself, not the attached PDF, so it's repeated here in plain text too.
// Own full-width block, margin-top matching the other section gaps in
// this template (16px) — not squeezed into the narrow Gesamtsumme table
// like an initial draft of the invoice version was before that got
// widened per feedback.
function vorkasseNotice(orderNumber: string, seller: CompanySettings | null): string {
const bankLine = seller && (seller.iban || seller.bic)
? [seller.bankName, seller.iban && `IBAN ${seller.iban}`, seller.bic && `BIC ${seller.bic}`].filter(Boolean).join(" · ")
: null;
return `
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin-top:20px;background:${BG_MUTED};border-radius:8px;padding:16px 20px;">
<tr>
<td style="font-size:13px;line-height:1.6;color:${TEXT_MUTED};text-align:center;">
Bitte überweise den Rechnungsbetrag unter Angabe der Bestellnummer ${escapeHtml(orderNumber)}${bankLine ? ` auf folgende Bankverbindung: <strong style="color:${TEXT_PRIMARY};">${escapeHtml(bankLine)}</strong>` : " auf die dir genannte Bankverbindung"}. Deine Bestellung wird nach Zahlungseingang bearbeitet (in der Regel innerhalb von 12 Werktagen).
</td>
</tr>
</table>
`;
}
// Live-Preview-only fallback (no real order/company-settings fetch there,
// see /email-preview/[type]) — the actual send always passes the real
// seller (company-settings) through buildLegalFooterLines() below. Email
@@ -181,6 +204,14 @@ export type OrderConfirmationData = {
discountAmount: number;
discountCode: string | null;
total: number;
// Explicit boolean set by each caller (checkout route's manual branch:
// true; the Stripe webhook path: always false, since only a *paid*
// Stripe order ever reaches this send) — not derived from
// paymentMethodTitle here, since that string ("Online-Zahlung",
// "Kreditkarte", "Überweisung (Vorkasse)", ...) is exactly the kind of
// fragile thing a payment-methods rename already broke once this
// session (see @einfach-produktiv/invoicing's isPaidImmediately()).
isManualPayment: boolean;
};
export const SAMPLE_ORDER: OrderConfirmationData = {
@@ -202,6 +233,7 @@ export const SAMPLE_ORDER: OrderConfirmationData = {
discountAmount: 5,
discountCode: "WILLKOMMEN10",
total: 37.7,
isManualPayment: false,
};
export function renderOrderConfirmationHtml(template: EmailTemplateContent, order: OrderConfirmationData, seller: CompanySettings | null): string {
@@ -263,6 +295,7 @@ export function renderOrderConfirmationHtml(template: EmailTemplateContent, orde
</tr>
${taxRows}
</table>
${order.isManualPayment ? vorkasseNotice(order.orderNumber, seller) : ""}
`;
return emailShell("✓", escapeHtml(template.heading), body, template.footerText, buildLegalFooterLines(seller));