1706da8598
- 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>
97 lines
3.4 KiB
TypeScript
97 lines
3.4 KiB
TypeScript
import type { Metadata } from "next";
|
|
import { Inter, Playfair_Display, Caveat, Lora } from "next/font/google";
|
|
import "./globals.css";
|
|
import { Navbar } from "./components/Navbar";
|
|
import { CartFlyProvider } from "./components/CartFly";
|
|
import { CartSync } from "./components/CartSync";
|
|
import { getProducts, getSeoSettings, getCompanySettings } from "./lib/payload";
|
|
import { buildOrganizationSchema } from "./lib/structuredData";
|
|
|
|
const inter = Inter({
|
|
variable: "--font-inter",
|
|
subsets: ["latin"],
|
|
weight: ["400", "500", "600", "700"],
|
|
});
|
|
|
|
const playfair = Playfair_Display({
|
|
variable: "--font-playfair",
|
|
subsets: ["latin"],
|
|
weight: ["600", "700", "800"],
|
|
});
|
|
|
|
const caveat = Caveat({
|
|
variable: "--font-caveat",
|
|
subsets: ["latin"],
|
|
weight: ["700"],
|
|
});
|
|
|
|
const lora = Lora({
|
|
variable: "--font-lora",
|
|
subsets: ["latin"],
|
|
weight: ["400", "600"],
|
|
});
|
|
|
|
// Backend-driven since 2026-07-24 (CompanySettings' "SEO" tab) — the
|
|
// literal strings below are only the fallback getSeoSettings() returns if
|
|
// that field is empty or unreachable, kept identical to what used to be
|
|
// hardcoded here so nothing changes until an admin actually fills in the
|
|
// new fields.
|
|
export async function generateMetadata(): Promise<Metadata> {
|
|
const seo = await getSeoSettings();
|
|
return {
|
|
metadataBase: new URL("https://einfach-produktiv.mk360.de"),
|
|
title: {
|
|
default: seo.defaultTitle ?? "einfach produktiv.",
|
|
template: seo.titleTemplate ?? "%s | einfach produktiv.",
|
|
},
|
|
description: seo.defaultDescription ?? undefined,
|
|
openGraph: {
|
|
siteName: "einfach produktiv.",
|
|
locale: "de_DE",
|
|
type: "website",
|
|
images: seo.defaultOgImage ? [{ url: seo.defaultOgImage }] : undefined,
|
|
},
|
|
twitter: {
|
|
card: "summary_large_image",
|
|
images: seo.defaultOgImage ? [seo.defaultOgImage] : undefined,
|
|
},
|
|
};
|
|
}
|
|
|
|
export default async function RootLayout({
|
|
children,
|
|
}: Readonly<{
|
|
children: React.ReactNode;
|
|
}>) {
|
|
// Same 60s-ISR-cached call every other page already makes — reused here
|
|
// just to know whether Navbar's "Shop" link should behave as an anchor
|
|
// to the homepage spotlight instead of a real /shop navigation (see
|
|
// Navbar.tsx/ProductSpotlight.tsx).
|
|
const [products, seller] = await Promise.all([getProducts(), getCompanySettings()]);
|
|
const singleActiveProduct = products.filter((p) => p.active).length === 1;
|
|
// Organization JSON-LD on every page — one canonical node (@id) that
|
|
// Product/Article schemas elsewhere link back to via `{ "@id": ... }`
|
|
// instead of repeating the full seller object per page (see
|
|
// structuredData.ts's own comment). getCompanySettings() is already the
|
|
// established pattern for a public page needing seller data server-side
|
|
// (see /impressum) — only non-sensitive fields (name/address/email/
|
|
// vatID) ever make it into the rendered schema, never iban/bic.
|
|
const organizationSchema = buildOrganizationSchema(seller);
|
|
|
|
return (
|
|
<html
|
|
lang="de"
|
|
className={`${inter.variable} ${playfair.variable} ${caveat.variable} ${lora.variable} h-full antialiased scroll-smooth`}
|
|
>
|
|
<body className="min-h-full flex flex-col">
|
|
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema) }} />
|
|
<CartFlyProvider>
|
|
<CartSync />
|
|
<Navbar singleActiveProduct={singleActiveProduct} />
|
|
{children}
|
|
</CartFlyProvider>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|