Files
Wabenhain/src/app/layout.tsx
T
Nick SzaunigandClaude Opus 5.5 839bbff595 feat: sichtbare Rueckmeldung fuer alle Aktionen (Toast statt URL-Meldungen)
Viele Aktionen gaben keine Rueckmeldung (Retoure annehmen, Archiv,
Bewertungen, Adressen, Newsletter, Kundensperre ...) oder meldeten Erfolg,
obwohl die zugehoerige E-Mail nie rausging.

- lib/flash: Meldung als kurzlebiges Cookie; Toast im Root-Layout
  (FlashToast). Ersetzt ?message=/?error= in der URL — darueber liess sich
  bisher beliebiger Text als Systemmeldung im Wabenhain-Layout anzeigen
  (Phishing-Hebel). Server Components leiten ueber /api/hinweis mit festen
  Codes und festen Zielen weiter.
- fail()/succeed() zentral und asynchron; ein Quelltext-Test stellt sicher,
  dass jeder Aufruf mit await/return erfolgt (sonst liefe eine Action nach
  einem Fehler weiter).
- sendMail meldet, ob die Mail angenommen wurde; Aktionen mit Mail zeigen
  ehrlich an, wenn der Versand scheitert (z.B. "Link erneut senden").
- Jede Server Action meldet jetzt Erfolg oder den Grund des Abbruchs.
- SubmitFeedback: geklickter Knopf zeigt sofort einen Ladezustand und ist
  bis zur Antwort gegen Doppelklick gesperrt.
- 2FA: nach dem Einrichten zurueck ins Profil statt auf die Seite mit
  "Deaktivieren".

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-25 04:36:36 +02:00

146 lines
5.6 KiB
TypeScript

import type { Metadata } from "next";
import { Cormorant_Garamond, Inter } from "next/font/google";
import "./globals.css";
import { Header, CartDrawer } from "@/components/site-chrome";
import { Footer } from "@/components/site-footer";
import { CookieConsentBanner } from "@/components/cookie-consent";
import { FlashToast } from "@/components/flash-toast";
import { SubmitFeedback } from "@/components/submit-feedback";
import { readFlash } from "@/lib/flash";
import { getSiteUrl } from "@/lib/url";
import { getCompanyInfo } from "@/lib/company-info";
import { getSocialLinks } from "@/lib/social-links";
import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary";
import { I18nProvider } from "@/i18n/i18n-provider";
const cormorant = Cormorant_Garamond({
variable: "--font-cormorant",
subsets: ["latin"],
weight: ["400", "500", "600"],
});
const inter = Inter({
variable: "--font-inter",
subsets: ["latin"],
});
export async function generateMetadata(): Promise<Metadata> {
const company = await getCompanyInfo();
const title = `${company.name} | Regionaler Honig aus Altenmünster`;
return {
metadataBase: new URL(getSiteUrl()),
title: { default: title, template: `%s | ${company.name}` },
description: company.description,
openGraph: {
title,
description: company.description,
siteName: company.name,
locale: "de_DE",
type: "website",
},
twitter: { card: "summary_large_image", title, description: company.description },
};
}
export default async function RootLayout({ children }: LayoutProps<"/">) {
const locale = await getLocale();
const dict = getDictionary(locale);
const [company, socialLinks, flash] = await Promise.all([getCompanyInfo(), getSocialLinks(), readFlash()]);
const base = getSiteUrl();
// LocalBusiness-Schema fuer die gesamte Seite (einmal im Root-Layout statt
// auf jeder Seite einzeln) — hilft insbesondere bei lokaler Google-Suche
// ("Honig Altenmünster") und der Kartenanzeige. telephone ist hier bewusst
// NICHT enthalten: solange niemand im Admin-Bereich (Inhalte) eine echte
// Telefonnummer eingetragen hat, bliebe sonst der site-config-Platzhalter
// als strukturierte Daten faelschlich indexiert.
const localBusinessJsonLd = {
"@context": "https://schema.org",
"@type": "Store",
"@id": `${base}/#business`,
name: company.name,
description: company.description,
url: base,
address: {
"@type": "PostalAddress",
streetAddress: company.address.street,
postalCode: company.address.zip,
addressLocality: company.address.city,
addressCountry: "DE",
},
};
// Ermoeglicht Googles Sitelinks-Suchfeld direkt in den Suchergebnissen — die
// Ziel-URL entspricht der tatsaechlich vorhandenen Shop-Suche (?q=...).
const websiteJsonLd = {
"@context": "https://schema.org",
"@type": "WebSite",
name: company.name,
url: base,
potentialAction: {
"@type": "SearchAction",
target: `${base}/shop?q={search_term_string}`,
"query-input": "required name=search_term_string",
},
};
return (
<html
lang={locale}
data-scroll-behavior="smooth"
className={`${cormorant.variable} ${inter.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col bg-background text-ink">
{/* Rettungsnetz fuer den Fall, dass kein JavaScript laeuft.
Hero, ScrollReveal und ProductCard starten mit opacity 0 und werden
erst per motion/react eingeblendet. Weil diese Startwerte schon im
server-gerenderten HTML als inline-style stehen, blieb ohne
JavaScript alles unterhalb des Hero-Bildes dauerhaft unsichtbar —
kein Teilausfall, sondern der komplette Seiteninhalt. Auf
wabenhain.com waren das 38 Elemente.
!important ist hier notwendig und nicht nachlaessig: es gibt keine
andere Moeglichkeit, einen inline-style zu ueberschreiben. Die
Regel zielt ausschliesslich auf [data-reveal] — Ein- und
Ausblendungen in AnimatePresence (Warenkorb-Schublade, Menue,
Cookie-Banner) tragen das Attribut nicht und bleiben verborgen.
Deckt den Fall "JavaScript deaktiviert" ab. Laedt das Bundle
dagegen und schlaegt erst spaeter fehl, greift noscript nicht —
dafuer waere ein Zeitgeber noetig, der die Animation im Normalfall
stoeren wuerde. */}
<noscript>
<style>{`[data-reveal]{opacity:1!important;transform:none!important}`}</style>
</noscript>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(localBusinessJsonLd).replace(/</g, "\\u003c") }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteJsonLd).replace(/</g, "\\u003c") }}
/>
<I18nProvider locale={locale}>
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-50 focus:bg-forest focus:text-cream focus:px-4 focus:py-2 focus:text-sm"
>
{dict.nav.skipToContent}
</a>
<Header companyName={company.name} />
<main id="main-content" className="flex-1">
{children}
</main>
<Footer companyInfo={company} socialLinks={socialLinks} />
<CartDrawer />
<CookieConsentBanner />
<FlashToast flash={flash} />
<SubmitFeedback flashId={flash?.id ?? null} />
</I18nProvider>
</body>
</html>
);
}