Hero, ScrollReveal und ProductCard starten mit opacity 0 und werden per motion/react eingeblendet. Diese Startwerte stehen als inline-style bereits im server-gerenderten HTML — auf wabenhain.com an 38 Elementen nachgezaehlt. Laeuft kein JavaScript, blendet sie niemand wieder ein: der Besucher sieht das Hero-Bild und darunter eine leere Seite. Kein Teilausfall, sondern der komplette Inhalt. Fuer Suchmaschinen war das unkritisch, der Text steht im HTML. Fuer Menschen mit deaktiviertem JavaScript war es ein Totalausfall. Die drei Bausteine tragen jetzt data-reveal, und ein noscript-Block im Root-Layout hebt darueber die Startwerte auf. Das Attribut statt eines Treffers auf [style*="opacity:0"] ist der Kern der Sache: Warenkorb-Schublade, Menue und Cookie-Banner blenden ueber AnimatePresence ebenfalls mit opacity 0 ein, sollen ohne JavaScript aber verborgen BLEIBEN. Sie tragen das Attribut nicht. !important ist hier notwendig und nicht nachlaessig — einen inline-style ueberschreibt sonst nichts. Grenze der Loesung: noscript deckt "JavaScript deaktiviert" ab. Laedt das Bundle dagegen und schlaegt erst spaeter fehl, greift es nicht; dafuer waere ein Zeitgeber noetig, der die Animation im Normalfall stoeren wuerde. tsc: 0 Fehler. vitest: 359/359 gruen. eslint: 0 Warnungen. next build: gruen. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
141 lines
5.3 KiB
TypeScript
141 lines
5.3 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 { 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] = await Promise.all([getCompanyInfo(), getSocialLinks()]);
|
|
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 />
|
|
</I18nProvider>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|