perf: DB-Indizes, Footer als Server Component, Site-weite Fehlerseiten

Datenbank (prisma/migrations/20260904120000_add_performance_indexes):
7 fehlende Indizes auf Fremdschluessel-/Filter-Spalten ergaenzt
(Order.customerId+createdAt, Order.status, Order.createdAt,
Order.discountCodeId, OrderItem.orderId, Address.customerId,
AuditLog.createdAt) — bislang volle Tabellenscans bei Bestelluebersicht,
Admin-Dashboard, Checkout-Gutscheinpruefung und Audit-Log.

Bundle-Groesse: Footer (TikTok/YouTube/Instagram-Icons, Copyright-Zeile)
aus dem client-seitigen site-chrome.tsx in eine eigene Server Component
(site-footer.tsx) ausgelagert — brauchte "use client" nur wegen
useI18n(), jetzt getDictionary()-Server-Aufruf wie auf jeder Admin-Seite.
Entfernt ungenutztes clientseitiges JS/Hydration aus jedem Seitenaufruf
(Footer steht im Root-Layout).

UX: app/not-found.tsx und app/error.tsx ergaenzt (Next.js-Konventionen
fuer 404 bzw. Laufzeitfehler) — beide fehlten komplett, unbekannte Routen
und Rendering-Fehler zeigten bisher Next.js' generische, unbrandete
Standardseiten statt einer zur Seite passenden Nutzerfuehrung.
Dictionary-Kollision dabei aufgefallen und aufgeloest: eine bereits
bestehende, admin-interne errorPage-Uebersetzung (src/app/admin/error.tsx,
seit dem urspruenglichen App-Grundgeruest vorhanden) blieb unangetastet,
die neue site-weite Fehlerseite nutzt einen eigenen Schluessel
(siteErrorPage).

Verifiziert: tsc --noEmit clean, 118/118 Tests gruen, pnpm build clean.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Sonnet 5 committed 2026-09-04 09:07:37 +02:00
1 parent 9e8da18875
commit 04cc3061cf
13 files changed
+344 -215

No files matched your search

+24 -17
View File
@@ -1,34 +1,41 @@
"use client";
import { useEffect } from "react";
import { Container, Button, LinkButton } from "@/components/ui";
import { Container, SectionHeading, Button, LinkButton } from "@/components/ui";
import { useI18n } from "@/i18n/i18n-provider";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
/**
* Next.js' Datei-Konvention fuer Laufzeitfehler beim Rendern einer Seite —
* ohne diese Datei zeigt Next.js in Produktion eine generische, unbrandete
* Fehlerseite. MUSS eine Client Component sein (Next.js-Vorgabe fuer
* error.tsx); rendert innerhalb desselben Layout-Baums wie die restliche
* Seite, useI18n() ist daher verfuegbar (I18nProvider sitzt im Root-Layout
* oberhalb, siehe app/layout.tsx).
*/
export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
const { t } = useI18n();
useEffect(() => {
// Serverseitig geloggt Next.js Rendering-Fehler bereits automatisch;
// dieses Log dient nur der Sichtbarkeit im Browser fuer den Fall eines
// client-seitig ausgeloesten Fehlers.
console.error(error);
}, [error]);
return (
<div className="py-32">
<div className="py-24">
<Container className="max-w-xl text-center">
<p className="tracking-wide-caps text-xs text-honey-dark mb-3">{t.errorPage.eyebrow}</p>
<h1 className="text-3xl md:text-4xl text-forest">{t.errorPage.title}</h1>
<p className="mt-4 text-ink/70 leading-relaxed">{t.errorPage.description}</p>
<div className="mt-8 flex items-center justify-center gap-4">
<Button type="button" onClick={() => reset()}>
{t.errorPage.retry}
</Button>
<SectionHeading
eyebrow={t.siteErrorPage.eyebrow}
title={t.siteErrorPage.title}
align="center"
className="mx-auto"
/>
<p className="mt-6 text-ink/70">{t.siteErrorPage.text}</p>
<div className="mt-10 flex flex-wrap items-center justify-center gap-4">
<Button onClick={reset}>{t.siteErrorPage.retry}</Button>
<LinkButton href="/" variant="secondary">
{t.errorPage.backHome}
{t.siteErrorPage.backToHome}
</LinkButton>
</div>
</Container>
+2 -1
View File
@@ -1,7 +1,8 @@
import type { Metadata } from "next";
import { Cormorant_Garamond, Inter } from "next/font/google";
import "./globals.css";
import { Header, Footer, CartDrawer } from "@/components/site-chrome";
import { Header, CartDrawer } from "@/components/site-chrome";
import { Footer } from "@/components/site-footer";
import { CookieConsentBanner } from "@/components/cookie-consent";
import { siteConfig } from "@/lib/site-config";
import { getCompanyInfo } from "@/lib/company-info";
+22 -13
View File
@@ -1,22 +1,31 @@
import type { Metadata } from "next";
import { Container, LinkButton } from "@/components/ui";
import { Container, SectionHeading, LinkButton } from "@/components/ui";
import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary";
export const metadata: Metadata = { title: "Seite nicht gefunden", robots: { index: false } };
export const metadata: Metadata = { title: "Seite nicht gefunden" };
export default function NotFound() {
/**
* Next.js' Datei-Konvention fuer nicht gefundene Routen (notFound()-Aufrufe
* und unbekannte Pfade) — ohne diese Datei zeigt Next.js eine generische,
* unbrandete 404-Seite statt einer zur Seite passenden Nutzerfuehrung.
*/
export default async function NotFound() {
const t = getDictionary(await getLocale());
return (
<div className="py-32">
<div className="py-24">
<Container className="max-w-xl text-center">
<p className="tracking-wide-caps text-xs text-honey-dark mb-3">404</p>
<h1 className="text-3xl md:text-4xl text-forest">Diese Seite gibt es hier nicht.</h1>
<p className="mt-4 text-ink/70 leading-relaxed">
Der Link ist möglicherweise veraltet, oder die Seite wurde verschoben. Im Shop findest du
garantiert, wonach du suchst.
</p>
<div className="mt-8 flex items-center justify-center gap-4">
<LinkButton href="/shop">Zum Shop</LinkButton>
<SectionHeading
eyebrow={t.notFoundPage.eyebrow}
title={t.notFoundPage.title}
align="center"
className="mx-auto"
/>
<p className="mt-6 text-ink/70">{t.notFoundPage.text}</p>
<div className="mt-10 flex flex-wrap items-center justify-center gap-4">
<LinkButton href="/shop">{t.notFoundPage.backToShop}</LinkButton>
<LinkButton href="/" variant="secondary">
Zur Startseite
{t.notFoundPage.backToHome}
</LinkButton>
</div>
</Container>
+4 -179
View File
@@ -1,11 +1,11 @@
"use client";
import Link from "next/link";
import { useState, useEffect, useRef, type RefObject, type ReactNode } from "react";
import { useState, useEffect, useRef, type RefObject } from "react";
import { AnimatePresence, motion } from "motion/react";
import { useCartStore, cartItemCount, cartTotalCents } from "@/store/cart-store";
import { formatPrice } from "@/lib/format";
import { mainNav, footerNav } from "@/lib/site-config";
import { mainNav } from "@/lib/site-config";
import { Container, LinkButton } from "@/components/ui";
import { Logomark } from "@/components/logomark";
import { CartItemThumbnail } from "@/components/cart-item-thumbnail";
@@ -512,180 +512,5 @@ export function CartDrawer() {
);
}
// Erstes vollstaendiges Geschaeftsjahr — Startpunkt der im Footer
// angezeigten Copyright-Spanne "2023–<aktuelles Jahr>". Das Endjahr wird
// bewusst NICHT gespeichert, sondern bei jedem Render live aus
// new Date().getFullYear() berechnet, damit es nie per Hand nachgepflegt
// werden muss.
const FOUNDING_YEAR = 2023;
const SOCIAL_ICON_PROPS = { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.5 } as const;
function TikTokIcon() {
return (
<svg {...SOCIAL_ICON_PROPS} aria-hidden="true">
<path d="M14 4v10.5a3.5 3.5 0 1 1-3.5-3.5" />
<path d="M14 4c.3 1.8 1.6 3.6 4 4" />
</svg>
);
}
function YouTubeIcon() {
return (
<svg {...SOCIAL_ICON_PROPS} aria-hidden="true">
<rect x="2" y="5" width="20" height="14" rx="4" />
<path d="M10 9l6 3-6 3V9z" fill="currentColor" stroke="none" />
</svg>
);
}
function InstagramIcon() {
return (
<svg {...SOCIAL_ICON_PROPS} aria-hidden="true">
<rect x="3" y="3" width="18" height="18" rx="5" />
<circle cx="12" cy="12" r="4" />
<circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" stroke="none" />
</svg>
);
}
interface SocialLinks {
tiktok: string;
youtube: string;
instagram: string;
}
/**
* Ein Icon+Name-Link wird nur gerendert, wenn im Admin-Bereich (Inhalte ->
* Social Media) tatsaechlich eine URL hinterlegt ist — nie ein toter/
* Platzhalter-Link (siehe getSocialLinks()/content-registry.ts).
*/
function SocialLink({
href,
name,
icon,
}: {
href: string;
/** Plattformname (Eigenname, z.B. "TikTok") — unlokalisiert, dient als sichtbarer Linktext. */
name: string;
icon: ReactNode;
}) {
const { t } = useI18n();
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={t.footer.socialAriaLabel(name)}
className="flex items-center gap-1.5 text-cream/70 hover:text-cream transition-colors"
>
{icon}
<span className="text-xs">{name}</span>
</a>
);
}
export function Footer({
companyInfo,
socialLinks,
}: {
companyInfo: { name: string; email: string; address: { street: string; zip: string; city: string } };
socialLinks: SocialLinks;
}) {
const { t } = useI18n();
const hasAnySocialLink = socialLinks.tiktok || socialLinks.youtube || socialLinks.instagram;
const wabenhainLabels: Record<(typeof footerNav.wabenhain)[number]["key"], string> = {
aboutUs: t.footer.aboutUs,
origin: t.nav.origin,
harvests: t.nav.harvests,
contact: t.nav.contact,
};
const einkaufenLabels: Record<(typeof footerNav.einkaufen)[number]["key"], string> = {
shop: t.nav.shop,
businessCustomers: t.footer.businessCustomers,
trackOrder: t.footer.trackOrder,
};
const rechtlichesLabels: Record<(typeof footerNav.rechtliches)[number]["key"], string> = {
impressum: t.legalTitles.impressum,
datenschutz: t.legalTitles.datenschutz,
agb: t.legalTitles.agb,
widerruf: t.legalTitles.widerruf,
versandZahlung: t.legalTitles.versandZahlung,
cookieRichtlinie: t.legalTitles.cookieRichtlinie,
};
return (
<footer className="bg-forest text-cream mt-24">
<Container className="py-16">
<div className="grid grid-cols-2 md:grid-cols-5 gap-10">
<div className="col-span-2">
<div className="flex items-center gap-3">
<Logomark className="h-9 w-9" variant="inverted" />
<span className="text-2xl tracking-[0.2em] font-serif">
{companyInfo.name}
</span>
</div>
<p className="mt-3 text-cream/70 text-sm max-w-xs">{t.footer.tagline}</p>
<p className="mt-6 text-cream/60 text-sm">
{companyInfo.address.street}
<br />
{companyInfo.address.zip} {companyInfo.address.city}
<br />
{companyInfo.email}
</p>
</div>
<FooterColumn title={t.footer.columnWabenhain} links={footerNav.wabenhain} labels={wabenhainLabels} />
<FooterColumn title={t.footer.columnShopping} links={footerNav.einkaufen} labels={einkaufenLabels} />
<FooterColumn title={t.footer.columnLegal} links={footerNav.rechtliches} labels={rechtlichesLabels} />
</div>
<div className="mt-16 pt-8 border-t border-cream/15 flex flex-col md:flex-row md:items-center justify-between gap-4 text-xs text-cream/70">
<div className="flex flex-wrap items-center gap-x-6 gap-y-3">
<span>
© {FOUNDING_YEAR}–{new Date().getFullYear()} {companyInfo.name}
</span>
{hasAnySocialLink ? (
<div className="flex items-center gap-4">
{socialLinks.tiktok ? (
<SocialLink href={socialLinks.tiktok} name="TikTok" icon={<TikTokIcon />} />
) : null}
{socialLinks.youtube ? (
<SocialLink href={socialLinks.youtube} name="YouTube" icon={<YouTubeIcon />} />
) : null}
{socialLinks.instagram ? (
<SocialLink href={socialLinks.instagram} name="Instagram" icon={<InstagramIcon />} />
) : null}
</div>
) : null}
</div>
<span>{t.footer.bottomLine}</span>
</div>
</Container>
</footer>
);
}
function FooterColumn({
title,
links,
labels,
}: {
title: string;
links: readonly { href: string; key: string }[];
labels: Record<string, string>;
}) {
return (
<div>
<p className="tracking-wide-caps text-xs text-honey-light mb-4">{title}</p>
<ul className="space-y-2">
{links.map((link) => (
<li key={link.key}>
<Link href={link.href} className="text-sm text-cream/75 hover:text-cream">
{labels[link.key]}
</Link>
</li>
))}
</ul>
</div>
);
}
// Footer wurde nach @/components/site-footer.tsx ausgelagert (Server
// Component statt Teil dieser "use client"-Datei) — siehe Kommentar dort.
+191
View File
@@ -0,0 +1,191 @@
import Link from "next/link";
import type { ReactNode } from "react";
import { Container } from "@/components/ui";
import { Logomark } from "@/components/logomark";
import { footerNav } from "@/lib/site-config";
import type { SocialLinks } from "@/lib/social-links";
import { getDictionary } from "@/i18n/get-dictionary";
import { getLocale } from "@/i18n/locale";
import type { Dictionary } from "@/i18n/dictionary-type";
// Erstes vollstaendiges Geschaeftsjahr — Startpunkt der im Footer
// angezeigten Copyright-Spanne "2023–<aktuelles Jahr>". Das Endjahr wird
// bewusst NICHT gespeichert, sondern bei jedem Render live aus
// new Date().getFullYear() berechnet, damit es nie per Hand nachgepflegt
// werden muss.
const FOUNDING_YEAR = 2023;
const SOCIAL_ICON_PROPS = { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.5 } as const;
function TikTokIcon() {
return (
<svg {...SOCIAL_ICON_PROPS} aria-hidden="true">
<path d="M14 4v10.5a3.5 3.5 0 1 1-3.5-3.5" />
<path d="M14 4c.3 1.8 1.6 3.6 4 4" />
</svg>
);
}
function YouTubeIcon() {
return (
<svg {...SOCIAL_ICON_PROPS} aria-hidden="true">
<rect x="2" y="5" width="20" height="14" rx="4" />
<path d="M10 9l6 3-6 3V9z" fill="currentColor" stroke="none" />
</svg>
);
}
function InstagramIcon() {
return (
<svg {...SOCIAL_ICON_PROPS} aria-hidden="true">
<rect x="3" y="3" width="18" height="18" rx="5" />
<circle cx="12" cy="12" r="4" />
<circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" stroke="none" />
</svg>
);
}
/**
* Ein Icon+Name-Link wird nur gerendert, wenn im Admin-Bereich (Inhalte ->
* Social Media) tatsaechlich eine URL hinterlegt ist — nie ein toter/
* Platzhalter-Link (siehe getSocialLinks()/content-registry.ts).
*/
function SocialLink({
href,
name,
icon,
t,
}: {
href: string;
/** Plattformname (Eigenname, z.B. "TikTok") — unlokalisiert, dient als sichtbarer Linktext. */
name: string;
icon: ReactNode;
t: Dictionary;
}) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={t.footer.socialAriaLabel(name)}
className="flex items-center gap-1.5 text-cream/70 hover:text-cream transition-colors"
>
{icon}
<span className="text-xs">{name}</span>
</a>
);
}
/**
* Server Component (bewusst NICHT Teil von site-chrome.tsx/"use client"):
* der Footer ist bis auf die Uebersetzungen rein statisches Markup, das auf
* jeder Seite gerendert wird. Statt useI18n() (Client-Hook) wird die
* Uebersetzung wie bei anderen Server Components direkt per
* getDictionary(await getLocale()) aufgeloest — dadurch verlaesst dieser
* Block das Client-JS-Bundle vollstaendig (siehe Kommentar in
* i18n/i18n-provider.tsx zur RSC-Serialisierungsgrenze).
*/
export async function Footer({
companyInfo,
socialLinks,
}: {
companyInfo: { name: string; email: string; address: { street: string; zip: string; city: string } };
socialLinks: SocialLinks;
}) {
const t = getDictionary(await getLocale());
const hasAnySocialLink = socialLinks.tiktok || socialLinks.youtube || socialLinks.instagram;
const wabenhainLabels: Record<(typeof footerNav.wabenhain)[number]["key"], string> = {
aboutUs: t.footer.aboutUs,
origin: t.nav.origin,
harvests: t.nav.harvests,
contact: t.nav.contact,
};
const einkaufenLabels: Record<(typeof footerNav.einkaufen)[number]["key"], string> = {
shop: t.nav.shop,
businessCustomers: t.footer.businessCustomers,
trackOrder: t.footer.trackOrder,
};
const rechtlichesLabels: Record<(typeof footerNav.rechtliches)[number]["key"], string> = {
impressum: t.legalTitles.impressum,
datenschutz: t.legalTitles.datenschutz,
agb: t.legalTitles.agb,
widerruf: t.legalTitles.widerruf,
versandZahlung: t.legalTitles.versandZahlung,
cookieRichtlinie: t.legalTitles.cookieRichtlinie,
};
return (
<footer className="bg-forest text-cream mt-24">
<Container className="py-16">
<div className="grid grid-cols-2 md:grid-cols-5 gap-10">
<div className="col-span-2">
<div className="flex items-center gap-3">
<Logomark className="h-9 w-9" variant="inverted" />
<span className="text-2xl tracking-[0.2em] font-serif">
{companyInfo.name}
</span>
</div>
<p className="mt-3 text-cream/70 text-sm max-w-xs">{t.footer.tagline}</p>
<p className="mt-6 text-cream/60 text-sm">
{companyInfo.address.street}
<br />
{companyInfo.address.zip} {companyInfo.address.city}
<br />
{companyInfo.email}
</p>
</div>
<FooterColumn title={t.footer.columnWabenhain} links={footerNav.wabenhain} labels={wabenhainLabels} />
<FooterColumn title={t.footer.columnShopping} links={footerNav.einkaufen} labels={einkaufenLabels} />
<FooterColumn title={t.footer.columnLegal} links={footerNav.rechtliches} labels={rechtlichesLabels} />
</div>
<div className="mt-16 pt-8 border-t border-cream/15 flex flex-col md:flex-row md:items-center justify-between gap-4 text-xs text-cream/70">
<div className="flex flex-wrap items-center gap-x-6 gap-y-3">
<span>
© {FOUNDING_YEAR}–{new Date().getFullYear()} {companyInfo.name}
</span>
{hasAnySocialLink ? (
<div className="flex items-center gap-4">
{socialLinks.tiktok ? (
<SocialLink href={socialLinks.tiktok} name="TikTok" icon={<TikTokIcon />} t={t} />
) : null}
{socialLinks.youtube ? (
<SocialLink href={socialLinks.youtube} name="YouTube" icon={<YouTubeIcon />} t={t} />
) : null}
{socialLinks.instagram ? (
<SocialLink href={socialLinks.instagram} name="Instagram" icon={<InstagramIcon />} t={t} />
) : null}
</div>
) : null}
</div>
<span>{t.footer.bottomLine}</span>
</div>
</Container>
</footer>
);
}
function FooterColumn({
title,
links,
labels,
}: {
title: string;
links: readonly { href: string; key: string }[];
labels: Record<string, string>;
}) {
return (
<div>
<p className="tracking-wide-caps text-xs text-honey-light mb-4">{title}</p>
<ul className="space-y-2">
{links.map((link) => (
<li key={link.key}>
<Link href={link.href} className="text-sm text-cream/75 hover:text-cream">
{labels[link.key]}
</Link>
</li>
))}
</ul>
</div>
);
}
+14
View File
@@ -149,6 +149,20 @@ export const de: Dictionary = {
legalPage: {
reviewNotice: "[PLATZHALTER — vor Veröffentlichung rechtlich prüfen lassen]",
},
notFoundPage: {
eyebrow: "404",
title: "Diese Seite gibt es nicht — zumindest nicht mehr.",
text: "Der Link ist vielleicht veraltet, oder die Seite wurde verschoben. Schau dich im Shop um oder geh zurück zur Startseite.",
backToShop: "Zum Shop",
backToHome: "Zur Startseite",
},
siteErrorPage: {
eyebrow: "Fehler",
title: "Da ist etwas schiefgelaufen.",
text: "Das war ein technischer Fehler auf unserer Seite, nicht deine Schuld. Versuch es noch einmal, oder komm später wieder vorbei.",
retry: "Erneut versuchen",
backToHome: "Zur Startseite",
},
cookieConsent: {
dialogLabel: "Cookie-Einstellungen",
bannerTextBeforeLink:
+14
View File
@@ -149,6 +149,20 @@ export const en: Dictionary = {
legalPage: {
reviewNotice: "[PLACEHOLDER — must be reviewed by a lawyer before publication]",
},
notFoundPage: {
eyebrow: "404",
title: "This page doesn't exist — at least not anymore.",
text: "The link might be outdated, or the page has moved. Take a look around the shop, or head back to the homepage.",
backToShop: "Go to shop",
backToHome: "Back to homepage",
},
siteErrorPage: {
eyebrow: "Error",
title: "Something went wrong.",
text: "That was a technical error on our end, not something you did. Try again, or come back a bit later.",
retry: "Try again",
backToHome: "Back to homepage",
},
cookieConsent: {
dialogLabel: "Cookie settings",
bannerTextBeforeLink:
+14
View File
@@ -172,6 +172,20 @@ export interface Dictionary {
legalPage: {
reviewNotice: string;
};
notFoundPage: {
eyebrow: string;
title: string;
text: string;
backToShop: string;
backToHome: string;
};
siteErrorPage: {
eyebrow: string;
title: string;
text: string;
retry: string;
backToHome: string;
};
cookieConsent: {
dialogLabel: string;
bannerTextBeforeLink: string;
+1 -1
View File
@@ -68,7 +68,7 @@ export const CONTENT_REGISTRY: ContentFieldDef[] = [
{ key: "home.businessTeaser.paragraph", group: "home", labelDe: "Geschäftskunden-Hinweis: Text", labelEn: "Business teaser: Text", type: "textarea", localized: true, getDefault: (l) => getDictionary(l).home.businessTeaser.paragraph },
// --- Social Media (nicht lokalisiert, leer = Icon wird im Footer nicht
// angezeigt, siehe components/site-chrome.tsx Footer()) ---
// angezeigt, siehe components/site-footer.tsx Footer()) ---
{ key: "social.tiktok", group: "social", labelDe: "TikTok-Profil (URL)", labelEn: "TikTok profile (URL)", type: "text", localized: false, getDefault: () => "" },
{ key: "social.youtube", group: "social", labelDe: "YouTube-Kanal (URL)", labelEn: "YouTube channel (URL)", type: "text", localized: false, getDefault: () => "" },
{ key: "social.instagram", group: "social", labelDe: "Instagram-Profil (URL)", labelEn: "Instagram profile (URL)", type: "text", localized: false, getDefault: () => "" },
+4 -3
View File
@@ -29,9 +29,10 @@ export const siteConfig = {
} as const;
// "key" verweist auf i18n/dictionary-type.ts (nav.* bzw. footer.*/legalTitles.*)
// statt eines fest verdrahteten Labels — site-chrome.tsx loest den Text zur
// Laufzeit aus der aktiven Dictionary auf, damit Navigation und Footer mit
// der Sprachumschaltung mitziehen.
// statt eines fest verdrahteten Labels — site-chrome.tsx (Header/CartDrawer)
// bzw. site-footer.tsx (Footer) loesen den Text zur Laufzeit aus der aktiven
// Dictionary auf, damit Navigation und Footer mit der Sprachumschaltung
// mitziehen.
export const mainNav = [
{ href: "/shop", key: "shop" },
{ href: "/ernten", key: "harvests" },
+1 -1
View File
@@ -12,7 +12,7 @@ export interface SocialLinks {
* Admin-gepflegte Social-Media-Links (siehe admin/inhalte, Gruppe "Social
* Media") — nicht lokalisiert, wie Firmendaten (company-info.ts). Ein leerer
* String bedeutet "kein Profil hinterlegt"; der Footer rendert dann kein
* Icon fuer diese Plattform (siehe components/site-chrome.tsx Footer()).
* Icon fuer diese Plattform (siehe components/site-footer.tsx Footer()).
*/
export async function getSocialLinks(): Promise<SocialLinks> {
const v = await getContentValues([...SOCIAL_KEYS], "de");