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
@@ -0,0 +1,106 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { Container } from "@/components/ui";
|
||||
import { BackLink } from "@/components/back-link";
|
||||
import { wissenSlugs, siteConfig } from "@/lib/site-config";
|
||||
import { getCompanyInfo } from "@/lib/company-info";
|
||||
import { getLocale } from "@/i18n/locale";
|
||||
import { getDictionary } from "@/i18n/get-dictionary";
|
||||
import type { WissenSlug } from "@/i18n/dictionary-type";
|
||||
|
||||
function isWissenSlug(slug: string): slug is WissenSlug {
|
||||
return (wissenSlugs as readonly string[]).includes(slug);
|
||||
}
|
||||
|
||||
export async function generateMetadata(
|
||||
props: PageProps<"/wissen/[slug]">
|
||||
): Promise<Metadata> {
|
||||
const { slug } = await props.params;
|
||||
if (!isWissenSlug(slug)) return {};
|
||||
const t = getDictionary(await getLocale());
|
||||
const article = t.wissenArticles[slug];
|
||||
return {
|
||||
title: article.title,
|
||||
description: article.excerpt,
|
||||
alternates: { canonical: `/wissen/${slug}` },
|
||||
};
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return wissenSlugs.map((slug) => ({ slug }));
|
||||
}
|
||||
|
||||
export default async function WissenArticlePage(
|
||||
props: PageProps<"/wissen/[slug]">
|
||||
) {
|
||||
const { slug } = await props.params;
|
||||
if (!isWissenSlug(slug)) notFound();
|
||||
const t = getDictionary(await getLocale());
|
||||
const article = t.wissenArticles[slug];
|
||||
|
||||
const otherArticles = wissenSlugs.filter((s) => s !== slug).slice(0, 2);
|
||||
|
||||
const company = await getCompanyInfo();
|
||||
const base = `https://${siteConfig.domain}`;
|
||||
// Kein datePublished/image: fuer diese Artikel existiert kein echtes
|
||||
// Veroeffentlichungsdatum und keine echte Fotografie — beides zu erfinden
|
||||
// waere irrefuehrende strukturierte Daten statt hilfreicher.
|
||||
const jsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: article.title,
|
||||
description: article.excerpt,
|
||||
author: { "@type": "Organization", name: company.name },
|
||||
publisher: { "@type": "Organization", name: company.name },
|
||||
mainEntityOfPage: `${base}/wissen/${slug}`,
|
||||
};
|
||||
const breadcrumbJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
itemListElement: [
|
||||
{ "@type": "ListItem", position: 1, name: t.wissenListPage.eyebrow, item: `${base}/wissen` },
|
||||
{ "@type": "ListItem", position: 2, name: article.title, item: `${base}/wissen/${slug}` },
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="py-20">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd).replace(/</g, "\\u003c") }}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd).replace(/</g, "\\u003c") }}
|
||||
/>
|
||||
<Container className="max-w-2xl">
|
||||
<BackLink href="/wissen" label={t.wissenArticlePage.backLabel} />
|
||||
<p className="tracking-wide-caps text-xs text-honey-dark mb-3">{t.wissenArticlePage.eyebrow}</p>
|
||||
<h1 className="text-3xl text-forest">{article.title}</h1>
|
||||
<div className="mt-8 space-y-5 text-ink/75 leading-relaxed">
|
||||
{article.body.map((paragraph, i) => (
|
||||
<p key={i}>{paragraph}</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{otherArticles.length > 0 ? (
|
||||
<div className="mt-16 border-t border-ink/10 pt-10">
|
||||
<p className="text-sm tracking-wide-caps text-ink/60 mb-6">{t.wissenArticlePage.otherArticlesHeading}</p>
|
||||
<div className="space-y-6">
|
||||
{otherArticles.map((otherSlug) => {
|
||||
const other = t.wissenArticles[otherSlug];
|
||||
return (
|
||||
<Link key={otherSlug} href={`/wissen/${otherSlug}`} className="group block">
|
||||
<h2 className="text-lg text-forest group-hover:underline">{other.title}</h2>
|
||||
<p className="mt-1 text-sm text-ink/65 leading-relaxed">{other.excerpt}</p>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user