Files
anouma/app/(frontend)/layout.tsx
T
maro 65b6eeac51 Add privacy-friendly analytics and cookie consent, integrated into the CMS
Adds a first-party analytics system with no third-party trackers and no
persistent per-visitor identifiers: events are grouped into sessions via a
daily-rotating pseudonymous hash (IP+UA+host, HMAC-salted, never stored raw).

- New "Analytics" section in the Payload admin, gated by a canViewAnalytics
  permission on Users, following the existing custom-admin-view pattern
  used by the calendar feature
- Sessions, page views, unique visitors, top/entry/exit pages, events,
  device/browser breakdown, date range filter with period comparison
- Raw events and daily aggregate rollups as their own collections, with
  configurable retention and a cron endpoint that rolls up before purging
- Cookie consent banner (necessary/preferences/statistics) with categories
  and copy editable from a new CMS global; a track() helper that is a
  complete no-op without statistics consent, so nothing is ever sent
  before a visitor opts in
- track() calls wired into the booking flow, contact form, and navigation
2026-09-06 21:59:59 +02:00

101 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Metadata } from "next";
import { Cormorant_Garamond, Inter } from "next/font/google";
import { Navbar } from "@/components/Navbar";
import { Footer } from "@/components/Footer";
import { Analytics } from "@/components/Analytics";
import { siteConfig } from "@/lib/site";
import { getOffers } from "@/lib/payload/content";
import { getCurrentCustomer } from "@/lib/auth/customer";
import { getBookingSettingsGlobal, getContactGlobal, getCookieSettingsGlobal, getSEOSettingsGlobal } from "@/lib/payload/globals";
import { getSiteUrl } from "@/lib/seo/config";
import { JsonLd, organizationOrLocalBusinessJsonLd, websiteJsonLd } from "@/lib/seo/jsonld";
import "./globals.css";
// Every page under this layout can read live content from Payload, so the
// whole subtree is rendered dynamically rather than statically at build time.
export const dynamic = "force-dynamic";
const cormorant = Cormorant_Garamond({
variable: "--font-cormorant",
subsets: ["latin"],
weight: ["400", "500", "600"],
style: ["normal", "italic"],
});
const inter = Inter({
variable: "--font-inter",
subsets: ["latin"],
});
export const metadata: Metadata = {
metadataBase: new URL(siteConfig.domain),
title: {
default: siteConfig.title,
template: `%s – ${siteConfig.name}`,
},
description: siteConfig.description,
openGraph: {
type: "website",
locale: siteConfig.locale,
url: siteConfig.domain,
siteName: siteConfig.name,
title: siteConfig.title,
description: siteConfig.description,
},
twitter: {
card: "summary_large_image",
title: siteConfig.title,
description: siteConfig.description,
},
// Configurable via env only — never a hardcoded verification ID (see .env.example).
...(process.env.GOOGLE_SITE_VERIFICATION ? { verification: { google: process.env.GOOGLE_SITE_VERIFICATION } } : {}),
};
export default async function RootLayout({ children }: LayoutProps<"/">) {
const [offers, customer, seoSettings, bookingSettings, contact, siteUrl, cookieSettings] = await Promise.all([
getOffers(),
getCurrentCustomer(),
getSEOSettingsGlobal(),
getBookingSettingsGlobal(),
getContactGlobal(),
getSiteUrl(),
getCookieSettingsGlobal(),
]);
return (
<html
lang="de"
data-scroll-behavior="smooth"
className={`${cormorant.variable} ${inter.variable} h-full antialiased`}
>
<body className="flex min-h-full flex-col bg-background text-foreground">
<JsonLd
data={[
websiteJsonLd(siteUrl),
organizationOrLocalBusinessJsonLd({ settings: seoSettings, booking: bookingSettings, contact, siteUrl }),
]}
/>
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-[100] focus:rounded-full focus:bg-anouma-mauve-dark focus:px-5 focus:py-3 focus:text-anouma-cream-light"
>
Zum Inhalt springen
</a>
<Navbar offers={offers} customerName={customer?.name ?? null} />
<main id="main-content" className="flex-1">
{children}
</main>
<Footer />
<Analytics
consentVersion={cookieSettings.consentVersion}
preferencesEnabled={cookieSettings.preferences?.enabled !== false}
statisticsEnabled={cookieSettings.statistics?.enabled !== false}
necessary={{ label: cookieSettings.necessary?.label ?? "Notwendig", description: cookieSettings.necessary?.description ?? "" }}
preferences={{ label: cookieSettings.preferences?.label ?? "Präferenzen", description: cookieSettings.preferences?.description ?? "" }}
statistics={{ label: cookieSettings.statistics?.label ?? "Statistik", description: cookieSettings.statistics?.description ?? "" }}
/>
</body>
</html>
);
}