feat: Wabenhain Honig-Shop Anwendung
Vollstaendige Next.js/Prisma-E-Commerce-Anwendung fuer eine Imkerei: Shop, Warenkorb, Stripe-Checkout, Kundenkonto mit 2FA, Admin-Bereich (Produkte, Ernten, Chargen, Bestellungen, Gutscheine, admin-editierbare Website-Inhalte), i18n (DE/EN), Rate-Limiting/CSP/Audit-Log-Haertung, PostgreSQL- und MySQL-Unterstuetzung ueber austauschbare Prisma- Treiber-Adapter, Unit-/Komponenten-/E2E-Tests.
This commit is contained in:
1 parent
5c76fcc266
commit
c7927f2883
240 files changed
+26096
-137
No files matched your search
+101
-15
@@ -1,29 +1,115 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { Cormorant_Garamond, Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { Header, Footer, CartDrawer } from "@/components/site-chrome";
|
||||
import { CookieConsentBanner } from "@/components/cookie-consent";
|
||||
import { siteConfig } from "@/lib/site-config";
|
||||
import { getCompanyInfo } from "@/lib/company-info";
|
||||
import { getLocale } from "@/i18n/locale";
|
||||
import { getDictionary } from "@/i18n/get-dictionary";
|
||||
import { I18nProvider } from "@/i18n/i18n-provider";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
const cormorant = Cormorant_Garamond({
|
||||
variable: "--font-cormorant",
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "600"],
|
||||
});
|
||||
|
||||
const inter = Inter({
|
||||
variable: "--font-inter",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
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(`https://${siteConfig.domain}`),
|
||||
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 const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
export default async function RootLayout({ children }: LayoutProps<"/">) {
|
||||
const locale = await getLocale();
|
||||
const dict = getDictionary(locale);
|
||||
const company = await getCompanyInfo();
|
||||
|
||||
// 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": `https://${siteConfig.domain}/#business`,
|
||||
name: company.name,
|
||||
description: company.description,
|
||||
url: `https://${siteConfig.domain}`,
|
||||
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: `https://${siteConfig.domain}`,
|
||||
potentialAction: {
|
||||
"@type": "SearchAction",
|
||||
target: `https://${siteConfig.domain}/shop?q={search_term_string}`,
|
||||
"query-input": "required name=search_term_string",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
lang={locale}
|
||||
data-scroll-behavior="smooth"
|
||||
className={`${cormorant.variable} ${inter.variable} h-full antialiased`}
|
||||
>
|
||||
<body className="min-h-full flex flex-col">{children}</body>
|
||||
<body className="min-h-full flex flex-col bg-background text-ink">
|
||||
<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} />
|
||||
<CartDrawer />
|
||||
<CookieConsentBanner />
|
||||
</I18nProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user