Files
einfach-produktiv/app/layout.tsx
T
Marco bc40e22910 Gate search behind CompanySettings.searchEnabled; fix two reported bugs
Search icon now gated behind the new backend toggle, same pattern as
wishlistEnabled — off by default.

Fixed: blog category filter bar was invisible — it was wrapped in
<Reveal>, which sits right at/just past the hero's bottom edge, the
exact "already near the initial viewport" position Reveal.tsx's own
comment documents as a whileInView(margin:"-80px") trap (an element
already visible without scrolling can permanently never register as
"entered view", since `once: true` never gets a second chance). Now a
plain div, no scroll-reveal animation needed for a filter bar anyway.

Fixed: wishlist count showing one behind on the Navbar badge —
useWishlist.ts's toggle() broadcast the "refetch me" event immediately
after the optimistic local update, before the POST request was even
sent. Another instance's resulting refetch could race the actual
server-side write, get the pre-toggle list, and then never get told to
refetch again. Broadcast now only fires after the request settles.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 22:51:37 +00:00

102 lines
3.6 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, getWishlistEnabled, getSearchEnabled } 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, wishlistEnabled, searchEnabled] = await Promise.all([
getProducts(),
getCompanySettings(),
getWishlistEnabled(),
getSearchEnabled(),
]);
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} wishlistEnabled={wishlistEnabled} searchEnabled={searchEnabled} />
{children}
</CartFlyProvider>
</body>
</html>
);
}