609 lines
27 KiB
TypeScript
609 lines
27 KiB
TypeScript
import React from 'react'
|
|
import Image from 'next/image'
|
|
import Link from 'next/link'
|
|
import SeoNavbar from '@/components/seo/SeoNavbar'
|
|
import SeoFooter from '@/components/seo/SeoFooter'
|
|
import { BlogPost } from '@/lib/blogPosts'
|
|
|
|
const SITE = 'https://greenlenspro.com'
|
|
|
|
const blogUiCopy = {
|
|
en: {
|
|
home: 'Home',
|
|
blog: 'Blog',
|
|
quickAnswer: 'Quick Botanical Answer',
|
|
tableOfContents: 'Table of Contents',
|
|
updated: 'Updated',
|
|
actionPlan: 'Action Plan',
|
|
stepByStepGuide: 'Step-by-Step Action Guide',
|
|
questionsAndAnswers: 'Questions & Answers',
|
|
faqHeading: 'Frequently Asked Questions',
|
|
relatedGuides: 'Related Guides',
|
|
ctaEyebrow: 'Instant AI Health Diagnosis',
|
|
ctaTitle: 'Unsure what is wrong with your plant?',
|
|
ctaBody: 'Scan your plant leaf with GreenLens AI for an immediate disease check, pest identification, and custom care plan.',
|
|
ctaButton: 'Start Free Scan ➔',
|
|
appEyebrow: 'GreenLens Pro iOS App',
|
|
appTitle: 'Identify Any Plant & Get Custom Care Plans',
|
|
appBody: 'Scan your houseplants in seconds, diagnose leaf diseases, and get automatic watering reminders tailored to your space.',
|
|
appButton: 'Start First Scan Free — No Account Needed',
|
|
howCompiled: 'How this guide was compiled',
|
|
sources: 'Sources',
|
|
published: 'Published',
|
|
lastUpdated: 'Last updated',
|
|
writtenBy: 'Written by',
|
|
},
|
|
de: {
|
|
home: 'Start',
|
|
blog: 'Blog',
|
|
quickAnswer: 'Kurze Antwort',
|
|
tableOfContents: 'Inhaltsverzeichnis',
|
|
updated: 'Aktualisiert am',
|
|
actionPlan: 'Handlungsplan',
|
|
stepByStepGuide: 'Schritt-für-Schritt-Anleitung',
|
|
questionsAndAnswers: 'Fragen & Antworten',
|
|
faqHeading: 'Häufig gestellte Fragen',
|
|
relatedGuides: 'Weitere Ratgeber',
|
|
ctaEyebrow: 'Sofortige KI-Gesundheitsanalyse',
|
|
ctaTitle: 'Unsicher, was mit deiner Pflanze los ist?',
|
|
ctaBody: 'Scanne ein Blatt deiner Pflanze mit GreenLens AI für eine sofortige Diagnose, Schädlingserkennung und einen individuellen Pflegeplan.',
|
|
ctaButton: 'Kostenlosen Scan starten ➔',
|
|
appEyebrow: 'GreenLens Pro iOS App',
|
|
appTitle: 'Jede Pflanze erkennen & individuelle Pflegepläne erhalten',
|
|
appBody: 'Scanne deine Zimmerpflanzen in Sekunden, diagnostiziere Blattkrankheiten und erhalte automatische Gießerinnerungen, abgestimmt auf deinen Standort.',
|
|
appButton: 'Ersten Scan gratis starten — ohne Anmeldung',
|
|
howCompiled: 'Wie dieser Ratgeber entstanden ist',
|
|
sources: 'Quellen',
|
|
published: 'Veröffentlicht am',
|
|
lastUpdated: 'Zuletzt aktualisiert am',
|
|
writtenBy: 'Verfasst von',
|
|
},
|
|
es: {
|
|
home: 'Inicio',
|
|
blog: 'Blog',
|
|
quickAnswer: 'Respuesta rápida',
|
|
tableOfContents: 'Índice de contenidos',
|
|
updated: 'Actualizado',
|
|
actionPlan: 'Plan de acción',
|
|
stepByStepGuide: 'Guía paso a paso',
|
|
questionsAndAnswers: 'Preguntas y respuestas',
|
|
faqHeading: 'Preguntas frecuentes',
|
|
relatedGuides: 'Guías relacionadas',
|
|
ctaEyebrow: 'Diagnóstico instantáneo con IA',
|
|
ctaTitle: '¿No sabes qué le pasa a tu planta?',
|
|
ctaBody: 'Escanea una hoja de tu planta con GreenLens AI para un diagnóstico inmediato, detección de plagas y un plan de cuidado personalizado.',
|
|
ctaButton: 'Iniciar escaneo gratis ➔',
|
|
appEyebrow: 'GreenLens Pro para iOS',
|
|
appTitle: 'Identifica cualquier planta y recibe planes de cuidado personalizados',
|
|
appBody: 'Escanea tus plantas de interior en segundos, diagnostica enfermedades de las hojas y recibe recordatorios de riego automáticos adaptados a tu espacio.',
|
|
appButton: 'Haz tu primer escaneo gratis — sin cuenta',
|
|
howCompiled: 'Cómo se elaboró esta guía',
|
|
sources: 'Fuentes',
|
|
published: 'Publicado el',
|
|
lastUpdated: 'Última actualización',
|
|
writtenBy: 'Escrito por',
|
|
},
|
|
} as const
|
|
|
|
/**
|
|
* JSON-LD für Blogposts. BlogPosting liefert Autor und Datum, FAQPage und HowTo
|
|
* sind die beiden Formate, aus denen AI-Antwortsysteme direkt extrahieren.
|
|
* Ohne diese Blöcke bleibt der Beitrag für sie unstrukturierter Fließtext.
|
|
*/
|
|
function buildBlogSchema(post: BlogPost) {
|
|
const graph: Record<string, unknown>[] = [
|
|
{
|
|
'@type': 'BlogPosting',
|
|
'@id': `${SITE}${post.canonical}#article`,
|
|
headline: post.title,
|
|
description: post.metaDescription,
|
|
image: `${SITE}${post.heroImage}`,
|
|
datePublished: post.publishedAtIso,
|
|
dateModified: post.updatedAtIso || post.publishedAtIso,
|
|
inLanguage: post.locale,
|
|
mainEntityOfPage: { '@type': 'WebPage', '@id': `${SITE}${post.canonical}` },
|
|
author: {
|
|
'@type': 'Person',
|
|
name: post.author.name,
|
|
jobTitle: post.author.role,
|
|
},
|
|
publisher: {
|
|
'@type': 'Organization',
|
|
name: 'GreenLens',
|
|
url: SITE,
|
|
},
|
|
...(post.sources?.length
|
|
? { citation: post.sources.map((source) => ({ '@type': 'CreativeWork', name: source.title, url: source.url })) }
|
|
: {}),
|
|
},
|
|
]
|
|
|
|
if (post.faqs?.length) {
|
|
graph.push({
|
|
'@type': 'FAQPage',
|
|
'@id': `${SITE}${post.canonical}#faq`,
|
|
mainEntity: post.faqs.map((faq) => ({
|
|
'@type': 'Question',
|
|
name: faq.question,
|
|
acceptedAnswer: { '@type': 'Answer', text: faq.answer },
|
|
})),
|
|
})
|
|
}
|
|
|
|
if (post.howToSteps?.length) {
|
|
graph.push({
|
|
'@type': 'HowTo',
|
|
'@id': `${SITE}${post.canonical}#howto`,
|
|
name: post.howToName || post.title,
|
|
step: post.howToSteps.map((step, index) => ({
|
|
'@type': 'HowToStep',
|
|
position: index + 1,
|
|
name: step.name.replace(/^\d+\.\s*/, ''),
|
|
text: step.text,
|
|
})),
|
|
})
|
|
}
|
|
|
|
return { '@context': 'https://schema.org', '@graph': graph }
|
|
}
|
|
|
|
function renderFormattedText(text: string) {
|
|
if (!text) return null
|
|
const regex = /\[([^\]]+)\]\(([^)]+)\)/g
|
|
const parts: React.ReactNode[] = []
|
|
let lastIndex = 0
|
|
let match: RegExpExecArray | null
|
|
|
|
while ((match = regex.exec(text)) !== null) {
|
|
if (match.index > lastIndex) {
|
|
parts.push(text.substring(lastIndex, match.index))
|
|
}
|
|
const linkText = match[1]
|
|
const url = match[2]
|
|
const isExternal = url.startsWith('http')
|
|
if (isExternal) {
|
|
parts.push(
|
|
<a
|
|
key={match.index}
|
|
href={url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-emerald-700 underline underline-offset-2 hover:text-emerald-900 font-semibold"
|
|
>
|
|
{linkText}
|
|
</a>
|
|
)
|
|
} else {
|
|
parts.push(
|
|
<Link
|
|
key={match.index}
|
|
href={url}
|
|
className="text-emerald-700 underline underline-offset-2 hover:text-emerald-900 font-semibold"
|
|
>
|
|
{linkText}
|
|
</Link>
|
|
)
|
|
}
|
|
lastIndex = regex.lastIndex
|
|
}
|
|
|
|
if (lastIndex < text.length) {
|
|
parts.push(text.substring(lastIndex))
|
|
}
|
|
|
|
return parts.length > 0 ? parts : text
|
|
}
|
|
|
|
export function BlogPostTemplate({ post }: { post: BlogPost }) {
|
|
const t = blogUiCopy[post.locale] ?? blogUiCopy.en
|
|
|
|
return (
|
|
<div className="bg-[#fdfbf6] text-on-surface font-body antialiased min-h-screen relative overflow-hidden">
|
|
<script
|
|
type="application/ld+json"
|
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(buildBlogSchema(post)) }}
|
|
/>
|
|
|
|
{/* Navigation Header */}
|
|
<SeoNavbar locale={post.locale} />
|
|
|
|
{/* Decorative Green Glow Blobs */}
|
|
<div className="absolute inset-0 pointer-events-none -z-10" aria-hidden="true">
|
|
<div className="absolute top-[-5%] left-[-10%] w-[60vw] h-[60vw] max-w-[800px] max-h-[800px] bg-[#c2e5cc] blur-[120px] rounded-full mix-blend-multiply opacity-70" />
|
|
<div className="absolute top-[20%] right-[-5%] w-[50vw] h-[50vw] max-w-[600px] max-h-[600px] bg-[#e5f0cb] blur-[100px] rounded-full mix-blend-multiply opacity-80" />
|
|
<div className="absolute top-[50%] left-[10%] w-[40vw] h-[40vw] max-w-[500px] max-h-[500px] bg-[#a8e0b8] blur-[110px] rounded-full mix-blend-multiply opacity-60" />
|
|
</div>
|
|
|
|
{/* Main Container */}
|
|
<main className="pt-28 pb-20">
|
|
{/* Header / Breadcrumb & Meta */}
|
|
<header className="py-12 bg-white/80 backdrop-blur-md border-b border-emerald-900/10 shadow-xs">
|
|
<div className="max-w-[1320px] mx-auto px-6">
|
|
{/* Breadcrumbs */}
|
|
<nav className="flex items-center gap-2 text-xs text-emerald-800/70 mb-6 font-semibold tracking-wide">
|
|
<Link href="/" className="hover:text-emerald-700 transition-colors">
|
|
{t.home}
|
|
</Link>
|
|
<span>/</span>
|
|
<Link href="/blog" className="hover:text-emerald-700 transition-colors">
|
|
{t.blog}
|
|
</Link>
|
|
<span>/</span>
|
|
<span className="text-emerald-700 font-bold">{post.category}</span>
|
|
</nav>
|
|
|
|
{/* Category & Read Time */}
|
|
<div className="flex items-center gap-3 mb-5">
|
|
<span className="bg-emerald-600/10 text-emerald-800 border border-emerald-600/20 text-xs font-extrabold tracking-wider uppercase px-3.5 py-1 rounded-full shadow-xs">
|
|
🌿 {post.category}
|
|
</span>
|
|
<span className="text-xs text-emerald-900/70 font-semibold">
|
|
• {post.readTime}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Title & Subtitle */}
|
|
<h1 className="font-display text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight text-emerald-950 mb-5 leading-[1.15]">
|
|
{post.title}
|
|
</h1>
|
|
<p className="text-lg md:text-xl text-emerald-900/80 leading-relaxed max-w-[850px] mb-8 font-medium">
|
|
{post.subtitle}
|
|
</p>
|
|
|
|
{/* Author Info */}
|
|
<div className="flex items-center gap-4 pt-6 border-t border-emerald-900/10">
|
|
<div className="w-12 h-12 rounded-full bg-emerald-600 text-white font-bold text-lg flex items-center justify-center shadow-md border-2 border-emerald-400">
|
|
🌱
|
|
</div>
|
|
<div>
|
|
<div className="font-bold text-sm text-emerald-950">{post.author.name}</div>
|
|
<div className="text-xs text-emerald-700 font-medium">
|
|
{post.author.role} • {t.updated} {post.publishedAt}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Content & Sidebar Grid */}
|
|
<div className="max-w-[1320px] mx-auto px-6 pt-10 grid grid-cols-1 lg:grid-cols-12 gap-12">
|
|
{/* Main Article Content */}
|
|
<div className="lg:col-span-8">
|
|
{/* Featured Hero Image */}
|
|
<div className="relative w-full h-[360px] sm:h-[450px] rounded-2xl overflow-hidden shadow-2xl mb-10 border-2 border-emerald-900/10 group">
|
|
<Image
|
|
src={post.heroImage}
|
|
alt={post.heroImageAlt}
|
|
fill
|
|
priority
|
|
sizes="(min-width: 1024px) 66vw, 100vw"
|
|
className="object-cover"
|
|
/>
|
|
{/* Dynamic Hero Overlay Badge */}
|
|
<div className="absolute bottom-6 left-6 right-6 sm:right-auto max-w-[350px] bg-white/95 backdrop-blur-md p-4 rounded-xl border-2 border-emerald-600/30 flex items-center gap-3 shadow-2xl">
|
|
<div className="w-2 h-10 bg-emerald-600 rounded-full shrink-0"></div>
|
|
<div>
|
|
<div className="font-display font-bold text-sm text-emerald-950 mb-0.5">
|
|
{post.heroImageBadgeTitle}
|
|
</div>
|
|
<div className="text-[10px] font-extrabold tracking-[0.15em] uppercase text-emerald-700">
|
|
{post.heroImageBadgeLabel}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Direct Answer Summary Callout Box (AI-Overview Ready) */}
|
|
{post.directAnswer && (
|
|
<div className="bg-emerald-500/10 border-l-4 border-emerald-600 p-6 rounded-r-2xl mb-10 shadow-sm border-y border-r border-emerald-600/20">
|
|
<div className="flex items-center gap-2 text-emerald-800 font-extrabold text-xs uppercase tracking-wider mb-2">
|
|
<span>⚡ {t.quickAnswer}</span>
|
|
</div>
|
|
<p className="text-emerald-950 font-medium leading-relaxed text-base max-w-[72ch]">
|
|
{renderFormattedText(post.directAnswer)}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Mobile TOC */}
|
|
{post.toc && post.toc.length > 0 && (
|
|
<div className="lg:hidden bg-white p-6 rounded-2xl border-2 border-emerald-600/20 mb-10 shadow-sm">
|
|
<div className="font-extrabold text-xs text-emerald-800 uppercase tracking-widest mb-3">
|
|
{t.tableOfContents}
|
|
</div>
|
|
<ul className="space-y-2.5 text-sm font-medium">
|
|
{post.toc.map((item) => (
|
|
<li key={item.id}>
|
|
<a href={`#${item.id}`} className="text-emerald-700 hover:text-emerald-900 hover:underline">
|
|
{item.label}
|
|
</a>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
|
|
{/* Body Sections */}
|
|
<div className="space-y-12 text-emerald-950 text-base leading-relaxed">
|
|
{post.sections.map((section, sIdx) => (
|
|
<React.Fragment key={section.id}>
|
|
<section id={section.id} className="scroll-mt-28">
|
|
{section.eyebrow && (
|
|
<div className="text-xs font-extrabold tracking-widest text-emerald-700 uppercase mb-1">
|
|
{section.eyebrow}
|
|
</div>
|
|
)}
|
|
<h2 className="font-display text-2xl sm:text-3xl font-bold text-emerald-950 mb-4">
|
|
{section.title}
|
|
</h2>
|
|
<p className="text-emerald-900/80 text-base sm:text-lg leading-relaxed mb-6 font-normal max-w-[68ch]">
|
|
{renderFormattedText(section.body)}
|
|
</p>
|
|
|
|
{/* Bullets */}
|
|
{section.bullets && section.bullets.length > 0 && (
|
|
<ul className="space-y-3 mb-6 bg-white p-6 rounded-2xl border border-emerald-600/20 shadow-xs">
|
|
{section.bullets.map((bullet, idx) => (
|
|
<li key={idx} className="flex items-start gap-3 text-sm sm:text-base text-emerald-900">
|
|
<span className="w-2.5 h-2.5 rounded-full bg-emerald-600 mt-2 shrink-0 shadow-xs" />
|
|
<span className="max-w-[76ch]">{renderFormattedText(bullet)}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
|
|
{/* Table */}
|
|
{section.table && (
|
|
<div className="overflow-x-auto mb-8 rounded-2xl border-2 border-emerald-600/20 shadow-sm bg-white">
|
|
<table className="w-full text-left border-collapse text-sm">
|
|
<thead className="bg-emerald-900 text-white font-bold">
|
|
<tr>
|
|
{section.table.headers.map((h, i) => (
|
|
<th key={i} className="p-3.5 sm:p-4 border-b border-emerald-800">
|
|
{h}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-emerald-600/10">
|
|
{section.table.rows.map((row, rIdx) => (
|
|
<tr key={rIdx} className="hover:bg-emerald-500/5 transition-colors">
|
|
{row.map((cell, cIdx) => (
|
|
<td
|
|
key={cIdx}
|
|
className={`p-3.5 sm:p-4 text-emerald-900 ${
|
|
cIdx === 0 ? 'font-bold text-emerald-950' : ''
|
|
}`}
|
|
>
|
|
{renderFormattedText(cell)}
|
|
</td>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
|
|
{/* Callout */}
|
|
{section.callout && (
|
|
<div
|
|
className={`p-5 rounded-2xl border-2 mb-6 ${
|
|
section.callout.type === 'warning'
|
|
? 'bg-amber-500/10 border-amber-500/30 text-amber-950'
|
|
: 'bg-emerald-500/10 border-emerald-600/30 text-emerald-950'
|
|
}`}
|
|
>
|
|
<div className="font-bold text-sm mb-1">{section.callout.title}</div>
|
|
<div className="text-sm opacity-90">{renderFormattedText(section.callout.body)}</div>
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
{/* Mid-Content Conversion CTA Widget after 2nd section */}
|
|
{sIdx === 1 && (
|
|
<div className="my-10 p-6 sm:p-8 rounded-2xl bg-gradient-to-br from-emerald-900 via-emerald-950 to-ink text-white border-2 border-emerald-400/30 shadow-xl flex flex-col sm:flex-row items-center justify-between gap-6">
|
|
<div className="space-y-1.5 text-center sm:text-left">
|
|
<div className="flex items-center justify-center sm:justify-start gap-2 text-xs font-extrabold tracking-widest text-emerald-400 uppercase">
|
|
<span>🌿 {t.ctaEyebrow}</span>
|
|
</div>
|
|
<h3 className="font-display text-xl sm:text-2xl font-bold text-white">
|
|
{t.ctaTitle}
|
|
</h3>
|
|
<p className="text-white/80 text-sm max-w-[480px]">
|
|
{t.ctaBody}
|
|
</p>
|
|
</div>
|
|
<Link
|
|
href="/"
|
|
className="shrink-0 bg-emerald-500 hover:bg-emerald-400 text-ink font-bold px-6 py-3.5 rounded-full text-sm shadow-xl hover:scale-105 transition-all duration-200"
|
|
>
|
|
{t.ctaButton}
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</React.Fragment>
|
|
))}
|
|
|
|
{/* HowTo Step Guide */}
|
|
{post.howToSteps && post.howToSteps.length > 0 && (
|
|
<section
|
|
id="howto-step"
|
|
className="scroll-mt-28 bg-white p-6 sm:p-8 rounded-2xl border-2 border-emerald-600/20 shadow-md"
|
|
>
|
|
<div className="text-xs font-extrabold tracking-widest text-emerald-700 uppercase mb-2">
|
|
{t.actionPlan}
|
|
</div>
|
|
<h2 className="font-display text-2xl font-bold text-emerald-950 mb-6">
|
|
{post.howToName ?? t.stepByStepGuide}
|
|
</h2>
|
|
<div className="space-y-4">
|
|
{post.howToSteps.map((step, idx) => (
|
|
<div
|
|
key={idx}
|
|
className="flex items-start gap-4 p-4.5 rounded-xl bg-emerald-50/50 border border-emerald-600/20"
|
|
>
|
|
<div className="w-8 h-8 rounded-full bg-emerald-600 text-white font-bold flex items-center justify-center shrink-0 text-sm shadow-sm">
|
|
{idx + 1}
|
|
</div>
|
|
<div>
|
|
<div className="font-bold text-emerald-950 text-base mb-1">{step.name}</div>
|
|
<div className="text-emerald-900/80 text-sm">{step.text}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* FAQs */}
|
|
{post.faqs && post.faqs.length > 0 && (
|
|
<section id="faq" className="scroll-mt-28 pt-8 border-t-2 border-emerald-600/20">
|
|
<div className="text-xs font-extrabold tracking-widest text-emerald-700 uppercase mb-2">
|
|
{t.questionsAndAnswers}
|
|
</div>
|
|
<h2 className="font-display text-2xl sm:text-3xl font-bold text-emerald-950 mb-6">
|
|
{t.faqHeading}
|
|
</h2>
|
|
<div className="space-y-4">
|
|
{post.faqs.map((faq, idx) => (
|
|
<div key={idx} className="p-6 rounded-2xl bg-white border border-emerald-600/20 shadow-xs">
|
|
<h3 className="font-bold text-base sm:text-lg text-emerald-950 mb-2">
|
|
{faq.question}
|
|
</h3>
|
|
<p className="text-emerald-900/80 text-sm sm:text-base leading-relaxed">
|
|
{faq.answer}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* App Banner */}
|
|
<div className="p-8 sm:p-10 rounded-3xl bg-gradient-to-br from-emerald-900 via-emerald-950 to-ink text-white border-2 border-emerald-400/30 text-center shadow-2xl my-12 relative overflow-hidden">
|
|
<div className="absolute top-0 right-0 w-64 h-64 bg-emerald-500/10 blur-3xl rounded-full pointer-events-none" />
|
|
<span className="text-xs font-extrabold tracking-widest uppercase text-emerald-400 block mb-2">
|
|
{t.appEyebrow}
|
|
</span>
|
|
<h3 className="font-display text-2xl sm:text-3xl font-bold mt-1 mb-3 text-white">
|
|
{t.appTitle}
|
|
</h3>
|
|
<p className="text-white/80 text-sm sm:text-base max-w-[600px] mx-auto mb-6 leading-relaxed">
|
|
{t.appBody}
|
|
</p>
|
|
<a
|
|
href="https://apps.apple.com/app/greenlens"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center gap-2 bg-emerald-500 hover:bg-emerald-400 text-ink font-bold px-7 py-3.5 rounded-full shadow-xl hover:scale-105 transition-all duration-200"
|
|
>
|
|
<span>{t.appButton}</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sidebar */}
|
|
<aside className="lg:col-span-4 hidden lg:block">
|
|
<div className="sticky top-28 space-y-8">
|
|
{/* Table of Contents */}
|
|
{post.toc && post.toc.length > 0 && (
|
|
<div className="bg-white p-6 rounded-2xl border-2 border-emerald-600/20 shadow-sm">
|
|
<div className="font-extrabold text-xs uppercase tracking-widest text-emerald-800 mb-4">
|
|
Table of Contents
|
|
</div>
|
|
<nav className="space-y-2 text-sm font-medium">
|
|
{post.toc.map((item) => (
|
|
<a
|
|
key={item.id}
|
|
href={`#${item.id}`}
|
|
className="block text-emerald-900/70 hover:text-emerald-700 transition-colors py-1 border-l-2 border-transparent hover:border-emerald-600 pl-3"
|
|
>
|
|
{item.label}
|
|
</a>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
)}
|
|
|
|
{/* Related Articles */}
|
|
{post.relatedPosts && post.relatedPosts.length > 0 && (
|
|
<div className="bg-white p-6 rounded-2xl border-2 border-emerald-600/20 shadow-sm">
|
|
<div className="font-extrabold text-xs uppercase tracking-widest text-emerald-800 mb-4">
|
|
{t.relatedGuides}
|
|
</div>
|
|
<div className="space-y-4">
|
|
{post.relatedPosts.map((related) => (
|
|
<Link
|
|
key={related.slug}
|
|
href={`/blog/${related.slug}`}
|
|
className="group block p-3.5 rounded-xl hover:bg-emerald-50 border border-transparent hover:border-emerald-600/20 transition-all"
|
|
>
|
|
<div className="text-[10px] font-extrabold text-emerald-700 uppercase tracking-wider mb-1">
|
|
{related.category} • {related.readTime}
|
|
</div>
|
|
<div className="font-bold text-sm text-emerald-950 group-hover:text-emerald-700 transition-colors line-clamp-2 leading-snug">
|
|
{related.title}
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
|
|
{/* Methodik & Quellen — Vertrauenssignal, bewusst am Ende und vollständig sichtbar */}
|
|
{(post.methodology || post.sources?.length) && (
|
|
<div className="max-w-[1320px] mx-auto px-6 mt-16 lg:pr-[calc(33.333%+3rem)]">
|
|
<section className="bg-white/80 border border-emerald-900/10 rounded-2xl p-7">
|
|
{post.methodology && (
|
|
<>
|
|
<h2 className="text-sm font-extrabold uppercase tracking-widest text-emerald-800 mb-3">
|
|
{t.howCompiled}
|
|
</h2>
|
|
<p className="text-[15px] leading-relaxed text-emerald-950/80">{post.methodology}</p>
|
|
</>
|
|
)}
|
|
|
|
{post.sources && post.sources.length > 0 && (
|
|
<>
|
|
<h2 className="text-sm font-extrabold uppercase tracking-widest text-emerald-800 mt-7 mb-3">
|
|
{t.sources}
|
|
</h2>
|
|
<ul className="space-y-2">
|
|
{post.sources.map((source) => (
|
|
<li key={source.url} className="text-[15px] leading-relaxed">
|
|
<a
|
|
href={source.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-emerald-700 underline underline-offset-2 hover:text-emerald-900"
|
|
>
|
|
{source.title}
|
|
</a>
|
|
<span className="text-emerald-950/60"> — {source.publisher}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</>
|
|
)}
|
|
|
|
<p className="text-xs text-emerald-950/50 mt-6">
|
|
{t.published} {post.publishedAt}
|
|
{post.updatedAt ? ` · ${t.lastUpdated} ${post.updatedAt}` : ''} · {t.writtenBy} {post.author.name},{' '}
|
|
{post.author.role}
|
|
</p>
|
|
</section>
|
|
</div>
|
|
)}
|
|
</main>
|
|
|
|
{/* Site Footer */}
|
|
<SeoFooter locale={post.locale} />
|
|
</div>
|
|
)
|
|
}
|