feat(ui): Seitenabschluesse, Wissens-Verzeichnis, Kopfzeile ohne Ueberlauf

- Imkerei, Herkunft und Ernten endeten mit dem letzten Absatz direkt vor
  der Fusszeile. Neuer PageOutro: zwei naechste Wege als grosse
  Serif-Links mit Kurztext (Ernten/Shop bzw. Herkunft/Shop).
- Kopfzeile: bei 1280 px lief sie 54 px ueber (horizontale Scrollleiste),
  weil die nicht umbrechbare Slogan-Zeile unter dem Logo schon ab xl
  erschien. Jetzt erst ab 2xl; geprueft 1024 bis 1920 px ohne Ueberlauf.
- Wissen: Inhaltsverzeichnis statt zehn gleicher Bloecke mit je einem
  "Weiterlesen" - ganze Zeile ist der Link, Titel und Kurztext
  nebeneinander. Ungenutzter Schluessel readMore entfernt.
- Produktseite: bei ein, zwei Empfehlungen steht die Ueberschrift als
  erste Rasterspalte statt ueber einer einzelnen verlorenen Karte.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-09-26 02:05:25 +02:00
1 parent 908afebacb
commit b588975317
10 files changed
+118 -20

No files matched your search

+8
View File
@@ -8,6 +8,7 @@ import { getHarvests } from "@/lib/data";
import { formatMonthYear } from "@/lib/format"; import { formatMonthYear } from "@/lib/format";
import { getLocale } from "@/i18n/locale"; import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary"; import { getDictionary } from "@/i18n/get-dictionary";
import { PageOutro } from "@/components/page-outro";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Unsere Ernten", title: "Unsere Ernten",
@@ -96,6 +97,13 @@ export default async function ErntenPage() {
) : null} ) : null}
</div> </div>
</Container> </Container>
<PageOutro
ariaLabel={t.pageOutro.ariaLabel}
links={[
{ href: "/herkunft", label: t.nav.origin, text: t.pageOutro.originText },
{ href: "/shop", label: t.nav.shop, text: t.pageOutro.shopText },
]}
/>
</div> </div>
); );
} }
+8
View File
@@ -6,6 +6,7 @@ import { ScrollReveal } from "@/components/scroll-reveal";
import { getLocale } from "@/i18n/locale"; import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary"; import { getDictionary } from "@/i18n/get-dictionary";
import { getContentValue } from "@/lib/content"; import { getContentValue } from "@/lib/content";
import { PageOutro } from "@/components/page-outro";
export async function generateMetadata(): Promise<Metadata> { export async function generateMetadata(): Promise<Metadata> {
const t = getDictionary(await getLocale()); const t = getDictionary(await getLocale());
@@ -72,6 +73,13 @@ export default async function HerkunftPage() {
{t.herkunftPage.privacyNote} {t.herkunftPage.privacyNote}
</p> </p>
</Container> </Container>
<PageOutro
ariaLabel={t.pageOutro.ariaLabel}
links={[
{ href: "/ernten", label: t.nav.harvests, text: t.pageOutro.harvestsText },
{ href: "/shop", label: t.nav.shop, text: t.pageOutro.shopText },
]}
/>
</div> </div>
); );
} }
+8 -5
View File
@@ -525,15 +525,18 @@ export default async function ProductDetailPage(
</Container> </Container>
{relatedProducts.length > 0 ? ( {relatedProducts.length > 0 ? (
<Container className="mt-20"> // Bei ein, zwei Empfehlungen steht die Ueberschrift als erste Spalte im
<ScrollReveal> // Raster (wie "Aktuelle Ernte" auf der Startseite) — sonst stuende eine
<h2 className="text-3xl md:text-4xl text-forest mb-10">{t.productPage.relatedProductsHeading}</h2> // einzelne Karte verloren unter einer breiten Ueberschrift.
<Container className="mt-20 grid grid-cols-1 gap-x-8 gap-y-14 sm:grid-cols-2 lg:grid-cols-3">
<ScrollReveal
className={relatedProducts.length <= 2 ? "sm:col-span-2 lg:col-span-1" : "sm:col-span-2 lg:col-span-3"}
>
<h2 className="text-3xl md:text-4xl text-forest text-balance">{t.productPage.relatedProductsHeading}</h2>
</ScrollReveal> </ScrollReveal>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-14">
{relatedProducts.map((related, index) => ( {relatedProducts.map((related, index) => (
<ProductCard key={related.id} product={related} index={index} /> <ProductCard key={related.id} product={related} index={index} />
))} ))}
</div>
</Container> </Container>
) : null} ) : null}
+8
View File
@@ -5,6 +5,7 @@ import { ScrollReveal } from "@/components/scroll-reveal";
import { getLocale } from "@/i18n/locale"; import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary"; import { getDictionary } from "@/i18n/get-dictionary";
import { getContentValue } from "@/lib/content"; import { getContentValue } from "@/lib/content";
import { PageOutro } from "@/components/page-outro";
export async function generateMetadata(): Promise<Metadata> { export async function generateMetadata(): Promise<Metadata> {
const t = getDictionary(await getLocale()); const t = getDictionary(await getLocale());
@@ -55,6 +56,13 @@ export default async function ImkereiPage() {
/> />
</FramedMedia> </FramedMedia>
</Container> </Container>
<PageOutro
ariaLabel={t.pageOutro.ariaLabel}
links={[
{ href: "/ernten", label: t.nav.harvests, text: t.pageOutro.harvestsText },
{ href: "/shop", label: t.nav.shop, text: t.pageOutro.shopText },
]}
/>
</div> </div>
); );
} }
+18 -11
View File
@@ -19,36 +19,43 @@ export default async function WissenPage() {
const t = getDictionary(await getLocale()); const t = getDictionary(await getLocale());
return ( return (
<div className="py-20"> <div className="py-20">
<Container className="max-w-3xl"> <Container className="max-w-5xl">
<ScrollReveal> <ScrollReveal>
<SectionHeading level="h1" title={t.wissenListPage.title} /> <SectionHeading level="h1" title={t.wissenListPage.title} />
</ScrollReveal> </ScrollReveal>
<div className="mt-14 divide-y divide-ink/10 border-y border-ink/10"> {/* Inhaltsverzeichnis statt zehn gleicher Bloecke mit je einem
"Weiterlesen": die ganze Zeile ist der Link, Titel und Kurztext
stehen nebeneinander, der Pfeil zeigt die Richtung. */}
<ol className="mt-14 divide-y divide-ink/10 border-y border-ink/10">
{wissenSlugs.map((slug, i) => { {wissenSlugs.map((slug, i) => {
const article = t.wissenArticles[slug]; const article = t.wissenArticles[slug];
return ( return (
<ScrollReveal key={slug} delay={(i % 5) * 0.06}> <li key={slug}>
<Link href={`/wissen/${slug}`} className="group block py-9"> <ScrollReveal delay={(i % 5) * 0.05}>
<h2 className="link-draw inline pb-0.5 text-3xl leading-tight text-forest">{article.title}</h2> <Link
<p className="mt-3 max-w-[60ch] text-ink/70 leading-relaxed">{article.excerpt}</p> href={`/wissen/${slug}`}
<span className="mt-4 inline-flex items-center gap-2 text-sm text-honey-dark tracking-wide-caps"> className="group grid gap-3 py-8 md:grid-cols-12 md:items-baseline md:gap-8"
{t.wissenListPage.readMore} >
<h2 className="text-2xl leading-tight text-forest text-balance md:col-span-5 md:text-3xl">
<span className="link-draw pb-0.5">{article.title}</span>
</h2>
<p className="max-w-[60ch] text-ink/70 leading-relaxed md:col-span-6">{article.excerpt}</p>
<svg <svg
aria-hidden="true" aria-hidden="true"
viewBox="0 0 24 24" viewBox="0 0 24 24"
className="h-3.5 w-3.5 transition-transform duration-300 ease-[var(--ease-arrive)] group-hover:translate-x-1" className="hidden h-5 w-5 justify-self-end text-honey-dark transition-transform duration-300 ease-[var(--ease-arrive)] group-hover:translate-x-1 motion-reduce:transition-none md:col-span-1 md:block"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
strokeWidth="1.6" strokeWidth="1.6"
> >
<path d="M4 12h15M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" /> <path d="M4 12h15M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
</span>
</Link> </Link>
</ScrollReveal> </ScrollReveal>
</li>
); );
})} })}
</div> </ol>
</Container> </Container>
</div> </div>
); );
+49
View File
@@ -0,0 +1,49 @@
import Link from "next/link";
import { Container } from "@/components/ui";
export interface PageOutroLink {
href: string;
label: string;
text: string;
}
/**
* Abschluss einer Inhaltsseite (Imkerei, Herkunft, Ernten): zwei naechste
* Wege als grosse Serif-Links, getrennt durch eine Linie. Ohne ihn endeten
* diese Seiten mit dem letzten Absatz direkt vor der Fusszeile — eine
* Sackgasse, gerade fuer Besuch, der ueber die Suche mitten einsteigt.
*/
export function PageOutro({ links, ariaLabel }: { links: PageOutroLink[]; ariaLabel: string }) {
return (
<nav aria-label={ariaLabel} className="mt-24 border-t border-ink/10">
<Container className="grid md:grid-cols-2">
{links.map((link, i) => (
<Link
key={link.href}
href={link.href}
className={`group flex items-end justify-between gap-6 py-12 md:py-16 ${
i === 0 ? "md:pr-12" : "border-t border-ink/10 md:border-l md:border-t-0 md:pl-12"
}`}
>
<span>
<span className="block font-serif text-4xl leading-tight text-forest md:text-5xl">
{link.label}
</span>
<span className="mt-3 block max-w-[40ch] text-sm leading-relaxed text-ink/65">{link.text}</span>
</span>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
aria-hidden="true"
className="mb-2 h-6 w-6 flex-shrink-0 text-honey-dark transition-transform duration-500 ease-[var(--ease-arrive)] group-hover:translate-x-1.5 motion-reduce:transition-none"
>
<path d="M4 12h15M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Link>
))}
</Container>
</nav>
);
}
+1 -1
View File
@@ -84,7 +84,7 @@ function Logo({ t, companyName, logoSrc }: { t: Dictionary; companyName: string;
<span className="text-2xl tracking-[0.2em] text-forest font-serif"> <span className="text-2xl tracking-[0.2em] text-forest font-serif">
{companyName} {companyName}
</span> </span>
<span className="hidden xl:block text-[10px] tracking-[0.3em] uppercase text-honey-dark mt-1 whitespace-nowrap"> <span className="hidden 2xl:block text-[10px] tracking-[0.3em] uppercase text-honey-dark mt-1 whitespace-nowrap">
{t.footer.tagline} {t.footer.tagline}
</span> </span>
</span> </span>
+6 -1
View File
@@ -14,6 +14,12 @@ export const de: Dictionary = {
retry: "Erneut versuchen", retry: "Erneut versuchen",
}, },
}, },
pageOutro: {
ariaLabel: "Weiterlesen",
shopText: "Honig, Bienenwachskerzen und Propolis aus eigener Imkerei — solange die Charge reicht.",
harvestsText: "Jede Ernte mit Datum, Standort und Charge, zurückverfolgbar bis ins Glas.",
originText: "Wo unsere Bienenvölker stehen und was die Landschaft um Altenmünster prägt.",
},
nav: { nav: {
shop: "Shop", shop: "Shop",
harvests: "Unsere Ernten", harvests: "Unsere Ernten",
@@ -476,7 +482,6 @@ export const de: Dictionary = {
wissenListPage: { wissenListPage: {
eyebrow: "Honigwissen", eyebrow: "Honigwissen",
title: "Wissenswertes rund um Honig", title: "Wissenswertes rund um Honig",
readMore: "Weiterlesen",
}, },
wissenArticlePage: { wissenArticlePage: {
backLabel: "Zurück zum Honigwissen", backLabel: "Zurück zum Honigwissen",
+6 -1
View File
@@ -14,6 +14,12 @@ export const en: Dictionary = {
retry: "Try again", retry: "Try again",
}, },
}, },
pageOutro: {
ariaLabel: "Keep reading",
shopText: "Honey, beeswax candles and propolis from our own apiary — while each batch lasts.",
harvestsText: "Every harvest with date, location and batch, traceable down to the jar.",
originText: "Where our colonies live and what shapes the landscape around Altenmünster.",
},
nav: { nav: {
shop: "Shop", shop: "Shop",
harvests: "Our Harvests", harvests: "Our Harvests",
@@ -464,7 +470,6 @@ export const en: Dictionary = {
wissenListPage: { wissenListPage: {
eyebrow: "Honey Knowledge", eyebrow: "Honey Knowledge",
title: "Good to know about honey", title: "Good to know about honey",
readMore: "Read more",
}, },
wissenArticlePage: { wissenArticlePage: {
backLabel: "Back to Honey Knowledge", backLabel: "Back to Honey Knowledge",
+6 -1
View File
@@ -52,6 +52,12 @@ export interface Dictionary {
retry: string; retry: string;
}; };
}; };
pageOutro: {
ariaLabel: string;
shopText: string;
harvestsText: string;
originText: string;
};
nav: { nav: {
shop: string; shop: string;
harvests: string; harvests: string;
@@ -249,7 +255,6 @@ export interface Dictionary {
wissenListPage: { wissenListPage: {
eyebrow: string; eyebrow: string;
title: string; title: string;
readMore: string;
}; };
wissenArticlePage: { wissenArticlePage: {
backLabel: string; backLabel: string;