Files
Wabenhain/src/app/layout.tsx
T
Nick SzaunigandClaude Opus 5 cc267ad8d4 fix: ohne JavaScript blieb der gesamte Seiteninhalt unter dem Hero unsichtbar
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]>
2026-09-20 03:51:51 +02:00

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>
);
}