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]>
146 lines
5.6 KiB
TypeScript
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>
|
|
);
|
|
}
|