Files
Greenlens/greenlns-landing/components/SeoCategoryPage.tsx
2026-07-26 23:16:29 +02:00

183 lines
6.3 KiB
TypeScript

import SeoNavbar from '@/components/seo/SeoNavbar'
import SeoFooter from '@/components/seo/SeoFooter'
import {
SeoHero,
SeoAdvantage,
SeoMethodology,
SeoComparison,
SeoContentSections,
SeoBestFit,
SeoOfferBlock,
SeoFaq,
SeoRelated,
} from '@/components/seo/SeoSections'
import type { SeoPageProfile } from '@/lib/seoPages'
import { seoTemplateCopy } from '@/lib/seoTemplateCopy'
import { siteConfig, hasIosStoreUrl } from '@/lib/site'
interface SeoCategoryPageProps {
profile: SeoPageProfile
}
export default function SeoCategoryPage({ profile }: SeoCategoryPageProps) {
const locale = profile.locale ?? 'en'
const copy = seoTemplateCopy[locale]
const templateIntent = profile.templateIntent ?? 'identification'
const usesPhotoAnalysis = templateIntent === 'identification' || templateIntent === 'diagnosis'
const showsCareInsights = templateIntent === 'care' || templateIntent === 'watering'
const faqSchema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: profile.faqs.map((item) => ({
'@type': 'Question',
name: item.question,
acceptedAnswer: {
'@type': 'Answer',
text: item.answer,
},
})),
}
const appSchema = profile.includeAppSchema
? {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: siteConfig.name,
operatingSystem: 'iOS',
applicationCategory: 'LifestyleApplication',
description: profile.directAnswer,
...(hasIosStoreUrl && { downloadUrl: siteConfig.iosAppStoreUrl }),
// Kein pauschaler Nullpreis: Der Download ist kostenlos, die Nutzung nach
// vier Scans nicht. Apple liefert die regionalen Preise selbst aus.
offers: {
'@type': 'Offer',
category: 'Free trial, then subscription',
priceSpecification: {
'@type': 'PriceSpecification',
description:
'One scan free without an account, three more after signing up, then a monthly or yearly GreenLens Pro subscription. Current regional pricing is shown in the App Store.',
},
},
}
: null
// HowTo: bevorzugtes Format vieler KI-Antwortsysteme fuer "Wie erkenne ich ..."-Fragen
const howToSchema =
profile.howToSteps && profile.howToSteps.length > 0
? {
'@context': 'https://schema.org',
'@type': 'HowTo',
name: profile.howToName ?? profile.h1,
description: profile.directAnswer,
inLanguage: locale,
totalTime: 'PT5M',
step: profile.howToSteps.map((item, index) => ({
'@type': 'HowToStep',
position: index + 1,
name: item.name,
text: item.text,
})),
}
: null
// WebPage mit Aenderungsdatum, Autor und Sprache — Frische- und E-E-A-T-Signale
const webPageSchema = {
'@context': 'https://schema.org',
'@type': 'WebPage',
name: profile.h1,
description: profile.metaDescription,
url: `${siteConfig.domain}${profile.canonical}`,
inLanguage: locale,
dateModified: profile.lastUpdatedIso ?? undefined,
primaryImageOfPage: profile.heroImage
? { '@type': 'ImageObject', url: `${siteConfig.domain}${profile.heroImage}`, caption: profile.heroImageAlt }
: undefined,
isPartOf: {
'@type': 'WebSite',
name: siteConfig.name,
url: siteConfig.domain,
},
publisher: {
'@type': 'Organization',
name: siteConfig.name,
url: siteConfig.domain,
},
}
const breadcrumbSchema = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'Home',
item: siteConfig.domain,
},
{
'@type': 'ListItem',
position: 2,
name: profile.h1,
item: `${siteConfig.domain}${profile.canonical}`,
},
],
}
return (
<div className="bg-background text-on-background font-body antialiased">
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqSchema) }}
/>
{appSchema && (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(appSchema) }}
/>
)}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
/>
{howToSchema && (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(howToSchema) }}
/>
)}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(webPageSchema) }}
/>
<SeoNavbar locale={locale} methodologyHref={usesPhotoAnalysis ? '#methodology' : '#comparison'} />
<main className="pt-[72px] relative overflow-hidden bg-[#fdfbf6]">
{/* Dekorative Farb-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-[#ffd8b1] blur-[120px] rounded-full mix-blend-multiply opacity-60"></div>
<div className="absolute top-[15%] right-[-5%] w-[50vw] h-[50vw] max-w-[600px] max-h-[600px] bg-[#c2e5cc] blur-[100px] rounded-full mix-blend-multiply opacity-80"></div>
<div className="absolute top-[40%] left-[15%] w-[40vw] h-[40vw] max-w-[500px] max-h-[500px] bg-[#e5f0cb] blur-[100px] rounded-full mix-blend-multiply opacity-70"></div>
</div>
<SeoHero
profile={profile}
copy={copy}
secondaryHref={usesPhotoAnalysis ? '#methodology' : '#comparison'}
/>
<SeoAdvantage profile={profile} copy={copy} showInsights={showsCareInsights} />
<SeoContentSections profile={profile} />
{usesPhotoAnalysis && <SeoMethodology copy={copy} intent={templateIntent} />}
<SeoComparison profile={profile} copy={copy} intent={templateIntent} />
<SeoBestFit profile={profile} copy={copy} />
<SeoOfferBlock profile={profile} />
<SeoFaq profile={profile} copy={copy} />
<SeoRelated profile={profile} copy={copy} />
</main>
<SeoFooter locale={locale} showMethodology={usesPhotoAnalysis} />
</div>
)
}