TTikotok V4
This commit is contained in:
@@ -24,6 +24,11 @@ export default function Footer() {
|
||||
GREENLENS
|
||||
</Link>
|
||||
<p className="footer-brand-desc">{t.footer.brand}</p>
|
||||
<div style={{ display: 'flex', gap: '1rem', marginTop: '1.5rem', flexWrap: 'wrap' }}>
|
||||
<a href="https://apps.apple.com/de/app/plant-doctor-greenlens-pro/id6759843546?l=en-GB" target="_blank" rel="noopener noreferrer" style={{ transition: 'opacity 0.2s' }} onMouseOver={(e) => e.currentTarget.style.opacity = '0.8'} onMouseOut={(e) => e.currentTarget.style.opacity = '1'}>
|
||||
<img src="/app-store.png" alt="Download on the App Store" style={{ height: '40px', width: 'auto' }} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{t.footer.cols.map((col, ci) => (
|
||||
|
||||
161
greenlns-landing/components/GuideLinks.tsx
Normal file
161
greenlns-landing/components/GuideLinks.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useLang } from '@/context/LangContext'
|
||||
import type { Lang } from '@/lib/i18n'
|
||||
|
||||
interface GuideCard {
|
||||
href: string
|
||||
tag: string
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const heading: Record<Lang, { tag: string; title: string }> = {
|
||||
de: { tag: 'Guides', title: 'Pflanzen schneller erkennen und richtig handeln.' },
|
||||
en: { tag: 'Guides', title: 'Identify plants faster and act with confidence.' },
|
||||
es: { tag: 'Guías', title: 'Identifica plantas más rápido y actúa con claridad.' },
|
||||
}
|
||||
|
||||
const cards: Record<Lang, GuideCard[]> = {
|
||||
de: [
|
||||
{
|
||||
href: '/blumen-scanner',
|
||||
tag: 'Blumen',
|
||||
title: 'Blumen Scanner',
|
||||
description: 'Blumen per Foto erkennen, Namen erhalten und direkt Pflegehinweise nutzen.',
|
||||
},
|
||||
{
|
||||
href: '/pflanzen-bestimmen',
|
||||
tag: 'Pflanzen',
|
||||
title: 'Pflanzen bestimmen',
|
||||
description: 'Pflanze scannen, Artname sehen und den passenden Pflegeplan erhalten.',
|
||||
},
|
||||
{
|
||||
href: '/giess-erinnerung-app',
|
||||
tag: 'Gießerinnerung',
|
||||
title: 'Gieß-Erinnerung App',
|
||||
description: 'Nie wieder Gießen vergessen — Push-Erinnerungen pro Pflanze.',
|
||||
},
|
||||
{
|
||||
href: '/pflanzen-pflege-app',
|
||||
tag: 'Pflege',
|
||||
title: 'Pflanzen Pflege App',
|
||||
description: 'Pflegeplan, Push-Erinnerung und Gießrhythmus pro Pflanze verwalten.',
|
||||
},
|
||||
{
|
||||
href: '/vs/google-lens',
|
||||
tag: 'Google Lens',
|
||||
title: 'Google Pflanzen erkennen',
|
||||
description: 'Vergleiche Google Lens mit GreenLens für Pflanzenerkennung, Pflege und Diagnose.',
|
||||
},
|
||||
{
|
||||
href: '/zimmerpflanzen-bestimmen',
|
||||
tag: 'Zimmerpflanzen',
|
||||
title: 'Zimmerpflanzen bestimmen',
|
||||
description: 'Monstera, Efeutute oder Ficus? Zimmerpflanzen per Foto erkennen.',
|
||||
},
|
||||
{
|
||||
href: '/pflanzen-krankheiten-erkennen',
|
||||
tag: 'Diagnose',
|
||||
title: 'Pflanzenkrankheiten erkennen',
|
||||
description: 'Gelbe Blätter, braune Spitzen oder weiche Stiele einordnen und den nächsten Schritt finden.',
|
||||
},
|
||||
],
|
||||
en: [
|
||||
{
|
||||
href: '/plant-identifier-app',
|
||||
tag: 'Identify',
|
||||
title: 'Plant Identifier App',
|
||||
description: 'Scan any plant, get the species instantly, and move straight to care guidance.',
|
||||
},
|
||||
{
|
||||
href: '/identify-plant-photo',
|
||||
tag: 'By photo',
|
||||
title: 'Identify plant by photo',
|
||||
description: 'Use a plant photo or picture to get the name, care plan, and reminders.',
|
||||
},
|
||||
{
|
||||
href: '/plant-scanner',
|
||||
tag: 'Scanner',
|
||||
title: 'Plant Scanner',
|
||||
description: 'Point your camera at any plant and get name, care plan, and health check.',
|
||||
},
|
||||
{
|
||||
href: '/plant-disease-identifier',
|
||||
tag: 'Diagnosis',
|
||||
title: 'Plant Disease Identifier',
|
||||
description: 'Yellow leaves, brown tips, or soft stems? Analyze symptoms from a photo.',
|
||||
},
|
||||
{
|
||||
href: '/plant-care-app',
|
||||
tag: 'Care',
|
||||
title: 'Plant Care App',
|
||||
description: 'Care plans, watering reminders, and a collection for every plant you own.',
|
||||
},
|
||||
{
|
||||
href: '/best-plant-identification-app',
|
||||
tag: 'Compare',
|
||||
title: 'Best Plant Identification App',
|
||||
description: 'How GreenLens compares to other plant ID apps — and when to pick which.',
|
||||
},
|
||||
],
|
||||
es: [
|
||||
{
|
||||
href: '/es/identificador-de-plantas',
|
||||
tag: 'Identificar',
|
||||
title: 'Identificador de plantas',
|
||||
description: 'Escanea una planta y entiende qué necesita en segundos.',
|
||||
},
|
||||
{
|
||||
href: '/es/escaner-de-plantas',
|
||||
tag: 'Escáner',
|
||||
title: 'Escáner de plantas',
|
||||
description: 'Apunta la cámara a cualquier planta y recibe nombre, cuidados y diagnóstico.',
|
||||
},
|
||||
{
|
||||
href: '/es/app-para-cuidar-plantas',
|
||||
tag: 'Cuidado',
|
||||
title: 'App para cuidar plantas',
|
||||
description: 'Planes de cuidado, recordatorios de riego y tu colección en una sola app.',
|
||||
},
|
||||
{
|
||||
href: '/es/diagnosticar-enfermedades-plantas',
|
||||
tag: 'Diagnóstico',
|
||||
title: 'Diagnosticar enfermedades',
|
||||
description: '¿Hojas amarillas o puntas marrones? Analiza los síntomas con una foto.',
|
||||
},
|
||||
{
|
||||
href: '/es/comparar/google-lens',
|
||||
tag: 'Google Lens',
|
||||
title: 'GreenLens vs Google Lens',
|
||||
description: 'Compara Google Lens con GreenLens para identificación, cuidado y diagnóstico.',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default function GuideLinks() {
|
||||
const { lang } = useLang()
|
||||
const head = heading[lang]
|
||||
const items = cards[lang]
|
||||
|
||||
return (
|
||||
<section className="comparison-links" aria-labelledby="homepage-guides-heading">
|
||||
<div className="container">
|
||||
<div className="comparison-section-head">
|
||||
<p className="tag">{head.tag}</p>
|
||||
<h2 id="homepage-guides-heading">{head.title}</h2>
|
||||
</div>
|
||||
<div className="comparison-links-grid">
|
||||
{items.map((card) => (
|
||||
<Link key={card.href + card.title} href={card.href} className="comparison-link-card">
|
||||
<p className="comparison-mini-label">{card.tag}</p>
|
||||
<h3>{card.title}</h3>
|
||||
<p>{card.description}</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
50
greenlns-landing/components/LanguageSwitcher.tsx
Normal file
50
greenlns-landing/components/LanguageSwitcher.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
'use client'
|
||||
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useLang } from '@/context/LangContext'
|
||||
import type { Lang } from '@/lib/i18n'
|
||||
import { getAlternatePath, getLocaleForPath } from '@/lib/localeRoutes'
|
||||
|
||||
const LANGS: Lang[] = ['de', 'en', 'es']
|
||||
|
||||
interface LanguageSwitcherProps {
|
||||
/** 'landing' für die helle Pille auf der Landing-Navbar, 'seo' für die Subpage-Navbar. */
|
||||
variant?: 'landing' | 'seo'
|
||||
}
|
||||
|
||||
export default function LanguageSwitcher({ variant = 'landing' }: LanguageSwitcherProps) {
|
||||
const pathname = usePathname() ?? '/'
|
||||
const { setLang } = useLang()
|
||||
const active = getLocaleForPath(pathname)
|
||||
|
||||
const baseClass =
|
||||
variant === 'seo'
|
||||
? 'flex items-center bg-ink-soft rounded-full px-1 py-1 text-[10px] font-bold tracking-widest'
|
||||
: 'flex items-center gap-0.5 rounded-full p-[3px] bg-white/5 border border-white/10 text-[10px] font-bold tracking-widest'
|
||||
|
||||
const activeClass =
|
||||
variant === 'seo'
|
||||
? 'bg-[#4a5c4e] text-white rounded-full px-3 py-1 block'
|
||||
: 'bg-white/15 text-white rounded-full px-3 py-1 block'
|
||||
|
||||
const idleClass = 'text-white/50 hover:text-white px-3 py-1 transition-colors block'
|
||||
|
||||
return (
|
||||
<div className={baseClass} role="group" aria-label="Language selector">
|
||||
{LANGS.map((code) => {
|
||||
const isActive = active === code
|
||||
return (
|
||||
<a
|
||||
key={code}
|
||||
href={getAlternatePath(pathname, code)}
|
||||
className={isActive ? activeClass : idleClass}
|
||||
aria-current={isActive ? 'true' : undefined}
|
||||
onClick={() => setLang(code)}
|
||||
>
|
||||
{code.toUpperCase()}
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,23 +1,17 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useLang } from '@/context/LangContext'
|
||||
import type { Lang } from '@/lib/i18n'
|
||||
'use client'
|
||||
|
||||
const LANGS: { code: Lang; label: string; flag: string }[] = [
|
||||
{ code: 'de', label: 'DE', flag: 'DE' },
|
||||
{ code: 'en', label: 'EN', flag: 'EN' },
|
||||
{ code: 'es', label: 'ES', flag: 'ES' },
|
||||
]
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useLang } from '@/context/LangContext'
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher'
|
||||
|
||||
export default function Navbar() {
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const pathname = usePathname()
|
||||
const { lang, setLang, t } = useLang()
|
||||
const homeHref = (hash: string) => (pathname === '/' ? hash : `/${hash}`)
|
||||
export default function Navbar() {
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const pathname = usePathname()
|
||||
const { t } = useLang()
|
||||
const homeHref = (hash: string) => (pathname === '/' ? hash : `/${hash}`)
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 40)
|
||||
@@ -32,43 +26,18 @@ export default function Navbar() {
|
||||
GREENLENS
|
||||
</Link>
|
||||
|
||||
<div className={`nav-links${menuOpen ? ' nav-links--open' : ''}`}>
|
||||
<a href={homeHref('#features')} onClick={() => setMenuOpen(false)}>{t.nav.features}</a>
|
||||
<a href={homeHref('#intelligence')} onClick={() => setMenuOpen(false)}>{t.nav.tech}</a>
|
||||
<a href={homeHref('#faq')} onClick={() => setMenuOpen(false)}>FAQ</a>
|
||||
<a href={homeHref('#how')} onClick={() => setMenuOpen(false)}>{t.nav.how}</a>
|
||||
<Link href="/support" onClick={() => setMenuOpen(false)}>Support</Link>
|
||||
<a href={homeHref('#cta')} onClick={() => setMenuOpen(false)}>{t.nav.download}</a>
|
||||
<div className={`nav-links${menuOpen ? ' nav-links--open' : ''}`}>
|
||||
<a href={homeHref('#features')} onClick={() => setMenuOpen(false)}>{t.nav.features}</a>
|
||||
<a href={homeHref('#intelligence')} onClick={() => setMenuOpen(false)}>{t.nav.tech}</a>
|
||||
<a href={homeHref('#faq')} onClick={() => setMenuOpen(false)}>FAQ</a>
|
||||
<a href={homeHref('#how')} onClick={() => setMenuOpen(false)}>{t.nav.how}</a>
|
||||
<Link href="/support" onClick={() => setMenuOpen(false)}>Support</Link>
|
||||
<a href={homeHref('#cta')} onClick={() => setMenuOpen(false)}>{t.nav.download}</a>
|
||||
|
||||
<div className="lang-switcher" role="group" aria-label="Language selector">
|
||||
{LANGS.map((l) => (
|
||||
<button
|
||||
key={l.code}
|
||||
className={`lang-btn${lang === l.code ? ' lang-btn--active' : ''}`}
|
||||
onClick={() => {
|
||||
setLang(l.code)
|
||||
setMenuOpen(false)
|
||||
if (l.code === 'de' && pathname !== '/') {
|
||||
window.location.href = '/'
|
||||
}
|
||||
if (l.code === 'es' && !pathname.startsWith('/es')) {
|
||||
window.location.href = '/es'
|
||||
}
|
||||
if (l.code === 'en' && (pathname.startsWith('/de') || pathname.startsWith('/es'))) {
|
||||
window.location.href = '/'
|
||||
}
|
||||
}}
|
||||
aria-label={`Switch to ${l.label}`}
|
||||
aria-pressed={lang === l.code}
|
||||
>
|
||||
<span>{l.flag}</span>
|
||||
<span>{l.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<LanguageSwitcher />
|
||||
|
||||
<a href={homeHref('#cta')} className="nav-cta" onClick={() => setMenuOpen(false)}>{t.nav.cta}</a>
|
||||
</div>
|
||||
<a href={homeHref('#cta')} className="nav-cta" onClick={() => setMenuOpen(false)}>{t.nav.cta}</a>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="nav-hamburger"
|
||||
@@ -96,39 +65,6 @@ export default function Navbar() {
|
||||
gap: 1rem;
|
||||
border-top: 1px solid rgba(244,241,232,0.08);
|
||||
}
|
||||
.lang-switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
background: rgba(244,241,232,0.07);
|
||||
border: 1px solid rgba(244,241,232,0.12);
|
||||
border-radius: 999px;
|
||||
padding: 3px;
|
||||
}
|
||||
.lang-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: rgba(244,241,232,0.6);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.lang-btn:hover {
|
||||
color: rgba(244,241,232,0.9);
|
||||
background: rgba(244,241,232,0.1);
|
||||
}
|
||||
.lang-btn--active {
|
||||
background: rgba(244,241,232,0.15);
|
||||
color: #fff;
|
||||
}
|
||||
`}</style>
|
||||
</nav>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import Link from 'next/link'
|
||||
import Navbar from '@/components/Navbar'
|
||||
import CTA from '@/components/CTA'
|
||||
import Footer from '@/components/Footer'
|
||||
import SeoNavbar from '@/components/seo/SeoNavbar'
|
||||
import SeoFooter from '@/components/seo/SeoFooter'
|
||||
import {
|
||||
SeoHero,
|
||||
SeoAdvantage,
|
||||
SeoMethodology,
|
||||
SeoComparison,
|
||||
SeoContentSections,
|
||||
SeoBestFit,
|
||||
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 {
|
||||
@@ -10,62 +19,8 @@ interface SeoCategoryPageProps {
|
||||
}
|
||||
|
||||
export default function SeoCategoryPage({ profile }: SeoCategoryPageProps) {
|
||||
const labels = {
|
||||
en: {
|
||||
heroTag: 'GreenLens',
|
||||
primaryCta: 'Try GreenLens',
|
||||
secondaryCta: 'See full comparison',
|
||||
definition: 'Definition',
|
||||
updated: 'Last updated:',
|
||||
tableTag: 'At a glance',
|
||||
bestFit: 'Best fit',
|
||||
chooseIf: 'Choose GreenLens if:',
|
||||
notBestFit: 'Not the best fit',
|
||||
notRightIf: 'GreenLens is not the right tool if:',
|
||||
faqTag: 'FAQ',
|
||||
faqTitle: 'Common questions answered directly.',
|
||||
related: 'Related',
|
||||
supportTag: 'Need help?',
|
||||
supportTitle: 'Talk to GreenLens support',
|
||||
supportCopy: 'Questions about scans, care plans, billing, or features? Use the support page.',
|
||||
},
|
||||
de: {
|
||||
heroTag: 'GreenLens',
|
||||
primaryCta: 'GreenLens testen',
|
||||
secondaryCta: 'Vergleich ansehen',
|
||||
definition: 'Definition',
|
||||
updated: 'Aktualisiert:',
|
||||
tableTag: 'Überblick',
|
||||
bestFit: 'Passt gut',
|
||||
chooseIf: 'Wähle GreenLens, wenn:',
|
||||
notBestFit: 'Nicht ideal',
|
||||
notRightIf: 'GreenLens ist nicht die richtige Wahl, wenn:',
|
||||
faqTag: 'FAQ',
|
||||
faqTitle: 'Häufige Fragen direkt beantwortet.',
|
||||
related: 'Verwandt',
|
||||
supportTag: 'Brauchst du Hilfe?',
|
||||
supportTitle: 'GreenLens Support kontaktieren',
|
||||
supportCopy: 'Fragen zu Scans, Pflegeplänen, Abrechnung oder Funktionen? Nutze die Support-Seite.',
|
||||
},
|
||||
es: {
|
||||
heroTag: 'GreenLens',
|
||||
primaryCta: 'Probar GreenLens',
|
||||
secondaryCta: 'Ver comparación',
|
||||
definition: 'Definición',
|
||||
updated: 'Actualizado:',
|
||||
tableTag: 'Resumen',
|
||||
bestFit: 'Mejor opción',
|
||||
chooseIf: 'Elige GreenLens si:',
|
||||
notBestFit: 'No es ideal',
|
||||
notRightIf: 'GreenLens no es la herramienta adecuada si:',
|
||||
faqTag: 'FAQ',
|
||||
faqTitle: 'Preguntas frecuentes respondidas directamente.',
|
||||
related: 'Relacionado',
|
||||
supportTag: '¿Necesitas ayuda?',
|
||||
supportTitle: 'Contacta con soporte de GreenLens',
|
||||
supportCopy: '¿Preguntas sobre escaneos, planes de cuidado, facturación o funciones? Usa la página de soporte.',
|
||||
},
|
||||
}[profile.locale ?? 'en']
|
||||
const locale = profile.locale ?? 'en'
|
||||
const copy = seoTemplateCopy[locale]
|
||||
|
||||
const faqSchema = {
|
||||
'@context': 'https://schema.org',
|
||||
@@ -117,7 +72,7 @@ export default function SeoCategoryPage({ profile }: SeoCategoryPageProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-background text-on-background font-body antialiased">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqSchema) }}
|
||||
@@ -132,145 +87,28 @@ export default function SeoCategoryPage({ profile }: SeoCategoryPageProps) {
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
|
||||
/>
|
||||
<Navbar />
|
||||
<main className="comparison-page">
|
||||
{/* Hero */}
|
||||
<section className="comparison-hero">
|
||||
<div className="container comparison-hero-grid">
|
||||
<div className="comparison-hero-copy">
|
||||
<p className="tag">{labels.heroTag}</p>
|
||||
<h1>{profile.h1}</h1>
|
||||
<p className="comparison-lead">{profile.tagline}</p>
|
||||
<p>{profile.directAnswer}</p>
|
||||
<div className="comparison-actions">
|
||||
<a href="#cta" className="btn-primary">{labels.primaryCta}</a>
|
||||
<a href="#feature-table" className="btn-outline">{labels.secondaryCta}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside className="comparison-hero-card">
|
||||
<p className="comparison-card-label">{labels.definition}</p>
|
||||
<p>{profile.definitionBlock}</p>
|
||||
<p className="comparison-verified">{labels.updated} {profile.lastUpdated}</p>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
<SeoNavbar locale={locale} />
|
||||
|
||||
{profile.contentSections && profile.contentSections.length > 0 && (
|
||||
<section className="comparison-context">
|
||||
<div className="container comparison-context-grid">
|
||||
{profile.contentSections.map((section) => (
|
||||
<article key={section.title} className="comparison-context-card">
|
||||
<p className="tag">{section.eyebrow}</p>
|
||||
<h2>{section.title}</h2>
|
||||
<p>{section.body}</p>
|
||||
{section.bullets && (
|
||||
<ul className="comparison-bullet-list comparison-bullet-list--dark">
|
||||
{section.bullets.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
<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>
|
||||
|
||||
{/* Feature table */}
|
||||
<section className="comparison-table-section" id="feature-table">
|
||||
<div className="container">
|
||||
<div className="comparison-section-head">
|
||||
<p className="tag">{labels.tableTag}</p>
|
||||
<h2>{profile.featureTable.title}</h2>
|
||||
</div>
|
||||
|
||||
<div className="comparison-table">
|
||||
<div className="comparison-table-header">
|
||||
<span>Feature</span>
|
||||
<span>GreenLens</span>
|
||||
<span>{profile.featureTable.alternativeLabel}</span>
|
||||
</div>
|
||||
|
||||
{profile.featureTable.rows.map((row) => (
|
||||
<article key={row.feature} className="comparison-row">
|
||||
<div className="comparison-row-title">{row.feature}</div>
|
||||
<div className="comparison-cell comparison-cell--greenlens">{row.greenlens}</div>
|
||||
<div className="comparison-cell comparison-cell--competitor">{row.alternative}</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Fit cards */}
|
||||
<section className="comparison-fit">
|
||||
<div className="container comparison-fit-grid">
|
||||
<article className="comparison-fit-card comparison-fit-card--greenlens">
|
||||
<p className="tag">{labels.bestFit}</p>
|
||||
<h2>{labels.chooseIf}</h2>
|
||||
<ul className="comparison-bullet-list comparison-bullet-list--dark">
|
||||
{profile.greenLensIf.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article className="comparison-fit-card">
|
||||
<p className="tag">{labels.notBestFit}</p>
|
||||
<h2>{labels.notRightIf}</h2>
|
||||
<ul className="comparison-bullet-list comparison-bullet-list--dark">
|
||||
{profile.notBestIf.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FAQ */}
|
||||
<section className="comparison-faq">
|
||||
<div className="container">
|
||||
<div className="comparison-section-head">
|
||||
<p className="tag">{labels.faqTag}</p>
|
||||
<h2>{labels.faqTitle}</h2>
|
||||
</div>
|
||||
|
||||
<div className="comparison-faq-grid">
|
||||
{profile.faqs.map((item) => (
|
||||
<article key={item.question} className="comparison-faq-card">
|
||||
<h3>{item.question}</h3>
|
||||
<p>{item.answer}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Related links */}
|
||||
{profile.relatedLinks.length > 0 && (
|
||||
<section className="comparison-links">
|
||||
<div className="container comparison-links-grid">
|
||||
{profile.relatedLinks.map((link) => (
|
||||
<Link key={link.href} href={link.href} className="comparison-link-card">
|
||||
<p className="comparison-mini-label">{labels.related}</p>
|
||||
<h3>{link.label}</h3>
|
||||
<p>{link.description}</p>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
<Link href="/support" className="comparison-link-card comparison-link-card--support">
|
||||
<p className="comparison-mini-label">{labels.supportTag}</p>
|
||||
<h3>{labels.supportTitle}</h3>
|
||||
<p>{labels.supportCopy}</p>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<CTA />
|
||||
<SeoHero profile={profile} copy={copy} />
|
||||
<SeoAdvantage profile={profile} copy={copy} />
|
||||
<SeoMethodology copy={copy} />
|
||||
<SeoComparison profile={profile} copy={copy} />
|
||||
<SeoContentSections profile={profile} />
|
||||
<SeoBestFit profile={profile} copy={copy} />
|
||||
<SeoFaq profile={profile} copy={copy} />
|
||||
<SeoRelated profile={profile} copy={copy} />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
|
||||
<SeoFooter locale={locale} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
171
greenlns-landing/components/seo/SeoFooter.tsx
Normal file
171
greenlns-landing/components/seo/SeoFooter.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import Link from 'next/link'
|
||||
import type { Lang } from '@/lib/i18n'
|
||||
import { englishSeoPageSlugs, germanSeoPageSlugs, getSeoPageBySlug } from '@/lib/seoPages'
|
||||
import { spanishSeoPageProfiles } from '@/lib/spanishSeoPages'
|
||||
import { seoTemplateCopy } from '@/lib/seoTemplateCopy'
|
||||
import { siteConfig, hasIosStoreUrl, hasAndroidStoreUrl } from '@/lib/site'
|
||||
|
||||
interface SeoFooterProps {
|
||||
locale: Lang
|
||||
}
|
||||
|
||||
function slugLinks(slugs: readonly string[]) {
|
||||
return slugs
|
||||
.map((slug) => getSeoPageBySlug(slug))
|
||||
.filter((profile): profile is NonNullable<typeof profile> => Boolean(profile))
|
||||
.map((profile) => ({ href: profile.canonical, label: profile.h1 }))
|
||||
}
|
||||
|
||||
export default function SeoFooter({ locale }: SeoFooterProps) {
|
||||
const copy = seoTemplateCopy[locale].footer
|
||||
|
||||
const useCaseLinks = [
|
||||
...slugLinks(englishSeoPageSlugs),
|
||||
{ href: '/vs/picturethis', label: 'vs PictureThis' },
|
||||
{ href: '/vs/plantum', label: 'vs Plantum' },
|
||||
{ href: '/vs/inaturalist', label: 'vs iNaturalist' },
|
||||
{ href: '/vs/google-lens', label: 'vs Google Lens' },
|
||||
]
|
||||
const germanLinks = slugLinks(germanSeoPageSlugs)
|
||||
const spanishLinks = Object.values(spanishSeoPageProfiles).map((profile) => ({
|
||||
href: profile.canonical,
|
||||
label: profile.h1,
|
||||
}))
|
||||
|
||||
return (
|
||||
<footer className="bg-ink-deep text-white pt-24 pb-8 font-body overflow-hidden" id="download">
|
||||
<div className="max-w-[1200px] mx-auto px-6">
|
||||
{/* CTA + Bild */}
|
||||
<div className="flex flex-col lg:flex-row gap-12 mb-24 items-center">
|
||||
<div className="flex-1">
|
||||
<div className="text-primary-fixed font-bold tracking-widest text-xs mb-4 uppercase">{copy.downloadTag}</div>
|
||||
<h2 className="text-5xl md:text-6xl font-bold font-display leading-tight mb-6">
|
||||
{copy.ctaLine1}
|
||||
<br />
|
||||
{copy.ctaLine2}
|
||||
<br />
|
||||
<span className="italic text-primary-fixed">{copy.ctaEm}</span>
|
||||
</h2>
|
||||
<p className="text-white/80 max-w-md mb-10 text-sm leading-relaxed">{copy.ctaBody}</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 mb-6">
|
||||
{hasIosStoreUrl && (
|
||||
<a
|
||||
href={siteConfig.iosAppStoreUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:opacity-80 transition-opacity"
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src="/app-store.png" alt="Download on the App Store" className="h-12 w-auto" />
|
||||
</a>
|
||||
)}
|
||||
{hasAndroidStoreUrl && (
|
||||
<a
|
||||
href={siteConfig.androidPlayStoreUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:opacity-80 transition-opacity"
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src="/play-store.png" alt="Get it on Google Play" className="h-12 w-auto" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-white/60">
|
||||
{copy.storeNote}{' '}
|
||||
<Link href="/support" className="underline hover:text-white">
|
||||
{copy.supportLabel}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1 w-full relative h-[300px] lg:h-[400px] rounded-3xl overflow-hidden shadow-2xl">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src="/hero.jpg" alt="GreenLens" className="w-full h-full object-cover" />
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-ink-deep via-transparent to-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Marken- und Legal-Spalten */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12 mb-16 border-t border-white/10 pt-12">
|
||||
<div>
|
||||
<h3 className="font-display font-bold text-xl tracking-widest mb-4">GREENLENS</h3>
|
||||
<p className="text-xs text-white/70 leading-relaxed max-w-xs">{copy.brandBody}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-[10px] font-bold tracking-widest text-primary-fixed mb-6 uppercase">{copy.productTitle}</h4>
|
||||
<ul className="space-y-4 text-xs text-white/80">
|
||||
<li><a href="#advantage" className="hover:text-white">{seoTemplateCopy[locale].nav.features}</a></li>
|
||||
<li><a href="#methodology" className="hover:text-white">{seoTemplateCopy[locale].nav.tech}</a></li>
|
||||
<li><a href="#download" className="hover:text-white">{seoTemplateCopy[locale].nav.download}</a></li>
|
||||
<li><Link href="/support" className="hover:text-white">{copy.supportLabel}</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-[10px] font-bold tracking-widest text-primary-fixed mb-6 uppercase">{copy.companyTitle}</h4>
|
||||
<ul className="space-y-4 text-xs text-white/80">
|
||||
<li><a href="#methodology" className="hover:text-white">{seoTemplateCopy[locale].nav.how}</a></li>
|
||||
<li><a href="#faq" className="hover:text-white">FAQ</a></li>
|
||||
<li><Link href="/support" className="hover:text-white">{copy.supportLabel}</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-[10px] font-bold tracking-widest text-primary-fixed mb-6 uppercase">{copy.legalTitle}</h4>
|
||||
<ul className="space-y-4 text-xs text-white/80">
|
||||
<li><Link href="/imprint" className="hover:text-white">{copy.imprint}</Link></li>
|
||||
<li><Link href="/privacy" className="hover:text-white">{copy.privacy}</Link></li>
|
||||
<li><Link href="/terms" className="hover:text-white">{copy.terms}</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SEO-Link-Spalten (interne Verlinkung) */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-12 mb-24">
|
||||
<div>
|
||||
<h4 className="text-[10px] font-bold tracking-widest text-primary-fixed mb-6 uppercase">{copy.useCasesTitle}</h4>
|
||||
<ul className="space-y-3 text-xs text-white/70">
|
||||
{useCaseLinks.map((link) => (
|
||||
<li key={link.href}>
|
||||
<Link href={link.href} className="hover:text-white">{link.label}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-[10px] font-bold tracking-widest text-primary-fixed mb-6 uppercase">{copy.germanTitle}</h4>
|
||||
<ul className="space-y-3 text-xs text-white/70">
|
||||
{germanLinks.map((link) => (
|
||||
<li key={link.href}>
|
||||
<Link href={link.href} className="hover:text-white">{link.label}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-[10px] font-bold tracking-widest text-primary-fixed mb-6 uppercase">{copy.spanishTitle}</h4>
|
||||
<ul className="space-y-3 text-xs text-white/70">
|
||||
{spanishLinks.map((link) => (
|
||||
<li key={link.href}>
|
||||
<Link href={link.href} className="hover:text-white">{link.label}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Riesiger Schriftzug */}
|
||||
<div className="w-full overflow-hidden flex justify-center mb-12 border-t border-white/10 pt-16">
|
||||
<div className="text-[10vw] leading-none font-display font-bold text-white/[0.03] tracking-tighter select-none whitespace-nowrap" aria-hidden="true">
|
||||
GREENLENS
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row justify-between items-center text-[10px] text-white/50 border-t border-white/10 pt-8">
|
||||
<p>{copy.copy}</p>
|
||||
<div className="mt-4 md:mt-0 opacity-50">
|
||||
<span className="font-bold tracking-widest">GREENLENS</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
67
greenlns-landing/components/seo/SeoIcon.tsx
Normal file
67
greenlns-landing/components/seo/SeoIcon.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
/** Inline-SVG-Icons (ersetzen die Material-Symbols-Font des Design-Exports). */
|
||||
|
||||
export type SeoIconName =
|
||||
| 'check_circle'
|
||||
| 'calendar'
|
||||
| 'healing'
|
||||
| 'sun'
|
||||
| 'psychology'
|
||||
| 'menu'
|
||||
| 'close'
|
||||
|
||||
const paths: Record<SeoIconName, React.ReactNode> = {
|
||||
check_circle: (
|
||||
<>
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="m9 12 2 2 4-4" />
|
||||
</>
|
||||
),
|
||||
calendar: (
|
||||
<>
|
||||
<rect x="3" y="5" width="18" height="16" rx="2" />
|
||||
<path d="M8 3v4M16 3v4M3 10h18" />
|
||||
</>
|
||||
),
|
||||
healing: (
|
||||
<>
|
||||
<path d="M12 3v18M3 12h18" transform="rotate(45 12 12)" />
|
||||
<path d="M8.5 8.5h.01M15.5 15.5h.01" />
|
||||
</>
|
||||
),
|
||||
sun: (
|
||||
<>
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
|
||||
</>
|
||||
),
|
||||
psychology: (
|
||||
<>
|
||||
<path d="M12 4a7 7 0 0 1 7 7c0 2.4-1.2 4.5-3 5.7V20h-6v-2h-2a2 2 0 0 1-2-2v-2H4l1.6-2.7A7 7 0 0 1 12 4Z" />
|
||||
<circle cx="12" cy="11" r="2.2" />
|
||||
</>
|
||||
),
|
||||
menu: <path d="M4 6h16M4 12h16M4 18h16" />,
|
||||
close: <path d="M6 6l12 12M18 6L6 18" />,
|
||||
}
|
||||
|
||||
interface SeoIconProps {
|
||||
name: SeoIconName
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function SeoIcon({ name, className = 'w-6 h-6' }: SeoIconProps) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{paths[name]}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
96
greenlns-landing/components/seo/SeoNavbar.tsx
Normal file
96
greenlns-landing/components/seo/SeoNavbar.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher'
|
||||
import SeoIcon from '@/components/seo/SeoIcon'
|
||||
import type { Lang } from '@/lib/i18n'
|
||||
import { localeHome } from '@/lib/localeRoutes'
|
||||
import { seoTemplateCopy } from '@/lib/seoTemplateCopy'
|
||||
|
||||
interface SeoNavbarProps {
|
||||
locale: Lang
|
||||
}
|
||||
|
||||
export default function SeoNavbar({ locale }: SeoNavbarProps) {
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const copy = seoTemplateCopy[locale]
|
||||
|
||||
const links = [
|
||||
{ href: '#advantage', label: copy.nav.features },
|
||||
{ href: '#methodology', label: copy.nav.tech },
|
||||
{ href: '#faq', label: copy.nav.faq },
|
||||
{ href: '#methodology', label: copy.nav.how },
|
||||
{ href: '/support', label: copy.nav.support },
|
||||
]
|
||||
|
||||
return (
|
||||
<nav className="fixed top-0 w-full z-50 bg-ink text-white border-b border-ink-line">
|
||||
<div className="flex justify-between items-center px-6 py-4 max-w-[1200px] mx-auto">
|
||||
<Link
|
||||
href={localeHome[locale]}
|
||||
className="font-display text-2xl font-bold tracking-widest text-white"
|
||||
aria-label="GreenLens Home"
|
||||
>
|
||||
GREENLENS
|
||||
</Link>
|
||||
|
||||
{/* Desktop */}
|
||||
<div className="hidden lg:flex items-center gap-6">
|
||||
{links.map((link) =>
|
||||
link.href.startsWith('#') ? (
|
||||
<a key={link.label} className="text-white/80 hover:text-white transition-colors text-sm font-semibold" href={link.href}>
|
||||
{link.label}
|
||||
</a>
|
||||
) : (
|
||||
<Link key={link.label} className="text-white/80 hover:text-white transition-colors text-sm font-semibold" href={link.href}>
|
||||
{link.label}
|
||||
</Link>
|
||||
),
|
||||
)}
|
||||
<div className="ml-2">
|
||||
<LanguageSwitcher variant="seo" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<a
|
||||
className="hidden md:inline-flex bg-white text-ink px-6 py-2 rounded-full font-bold shadow-lg hover:bg-gray-200 transition-colors duration-300"
|
||||
href="#download"
|
||||
>
|
||||
{copy.nav.download}
|
||||
</a>
|
||||
<button
|
||||
className="lg:hidden text-white p-2"
|
||||
aria-label={menuOpen ? 'Close menu' : 'Open menu'}
|
||||
aria-expanded={menuOpen}
|
||||
onClick={() => setMenuOpen((open) => !open)}
|
||||
>
|
||||
<SeoIcon name={menuOpen ? 'close' : 'menu'} className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile */}
|
||||
{menuOpen && (
|
||||
<div className="lg:hidden bg-ink border-t border-ink-line px-6 py-6 flex flex-col gap-4">
|
||||
{links.map((link) =>
|
||||
link.href.startsWith('#') ? (
|
||||
<a key={link.label} className="text-white/80 hover:text-white text-sm font-semibold" href={link.href} onClick={() => setMenuOpen(false)}>
|
||||
{link.label}
|
||||
</a>
|
||||
) : (
|
||||
<Link key={link.label} className="text-white/80 hover:text-white text-sm font-semibold" href={link.href} onClick={() => setMenuOpen(false)}>
|
||||
{link.label}
|
||||
</Link>
|
||||
),
|
||||
)}
|
||||
<a className="text-white/80 hover:text-white text-sm font-semibold" href="#download" onClick={() => setMenuOpen(false)}>
|
||||
{copy.nav.download}
|
||||
</a>
|
||||
<LanguageSwitcher variant="seo" />
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
270
greenlns-landing/components/seo/SeoSections.tsx
Normal file
270
greenlns-landing/components/seo/SeoSections.tsx
Normal file
@@ -0,0 +1,270 @@
|
||||
import Link from 'next/link'
|
||||
import SeoIcon, { type SeoIconName } from '@/components/seo/SeoIcon'
|
||||
import type { SeoPageProfile } from '@/lib/seoPages'
|
||||
import type { SeoTemplateCopy } from '@/lib/seoTemplateCopy'
|
||||
|
||||
interface SectionProps {
|
||||
profile: SeoPageProfile
|
||||
copy: SeoTemplateCopy
|
||||
}
|
||||
|
||||
export function SeoHero({ profile, copy }: SectionProps) {
|
||||
return (
|
||||
<section className="max-w-[1200px] mx-auto px-6 py-16 grid grid-cols-1 lg:grid-cols-12 gap-12 items-start mt-8">
|
||||
<div className="lg:col-span-6 z-10 flex flex-col justify-center pt-8">
|
||||
<div className="font-body text-xs font-semibold tracking-[0.1em] uppercase text-primary mb-6 seo-fade">
|
||||
{copy.hero.eyebrow}
|
||||
</div>
|
||||
<h1 className="font-display font-bold text-[40px] leading-[1.2] tracking-[-0.01em] md:text-[56px] md:leading-[1.1] md:tracking-[-0.02em] text-primary mb-6 seo-fade seo-fade-1">
|
||||
{profile.h1}
|
||||
</h1>
|
||||
<p className="font-body text-lg text-on-surface-variant mb-4 leading-relaxed seo-fade seo-fade-2">{profile.tagline}</p>
|
||||
<p className="font-body text-base text-on-surface-variant mb-10 leading-relaxed seo-fade seo-fade-2">{profile.directAnswer}</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 seo-fade seo-fade-3">
|
||||
<a
|
||||
className="inline-flex justify-center items-center px-8 py-3 bg-ink text-white rounded-full font-bold shadow-lg hover:bg-ink-soft transition-colors duration-300"
|
||||
href="#download"
|
||||
>
|
||||
{copy.hero.ctaPrimary}
|
||||
</a>
|
||||
<a
|
||||
className="inline-flex justify-center items-center px-8 py-3 bg-transparent border-2 border-ink text-ink rounded-full font-bold hover:bg-ink hover:text-white transition-colors duration-300"
|
||||
href="#methodology"
|
||||
>
|
||||
{copy.hero.ctaSecondary}
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-12 grid grid-cols-2 gap-6 pt-8 border-t border-surface-container-high seo-fade seo-fade-3">
|
||||
<div>
|
||||
<div className="font-display text-2xl font-semibold text-primary mb-1">{copy.hero.stat1Title}</div>
|
||||
<div className="text-xs font-semibold tracking-[0.1em] uppercase text-on-surface-variant">{copy.hero.stat1Label}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-display text-2xl font-semibold text-primary mb-1">{copy.hero.stat2Title}</div>
|
||||
<div className="text-xs font-semibold tracking-[0.1em] uppercase text-on-surface-variant">{copy.hero.stat2Label}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg:col-span-6 flex justify-center items-center seo-fade seo-fade-2">
|
||||
<div className="relative w-full max-w-[320px] md:max-w-[380px] h-[550px] md:h-[650px] rounded-3xl overflow-hidden shadow-2xl border-[8px] border-surface-container">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img alt={copy.hero.imageAlt} className="w-full h-full object-cover" src="/hero.jpg" />
|
||||
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 w-[90%] bg-surface/95 backdrop-blur p-4 rounded-xl border border-surface-container-highest flex items-center gap-3 shadow-xl">
|
||||
<div className="w-1.5 h-8 bg-primary rounded-full"></div>
|
||||
<div>
|
||||
<div className="font-display font-semibold text-sm text-primary mb-0.5">{copy.hero.floatTitle}</div>
|
||||
<div className="text-[10px] font-semibold tracking-[0.1em] uppercase text-on-surface-variant">{copy.hero.floatLabel}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export function SeoAdvantage({ profile, copy }: SectionProps) {
|
||||
return (
|
||||
<section className="max-w-[1200px] mx-auto px-6 py-16 grid grid-cols-1 lg:grid-cols-3 gap-12 border-t border-surface-container-high" id="advantage">
|
||||
<div className="lg:col-span-2">
|
||||
<div className="mb-8">
|
||||
<span className="text-xs font-semibold tracking-[0.1em] uppercase text-primary">{copy.advantage.eyebrow}</span>
|
||||
</div>
|
||||
<h2 className="font-display font-semibold text-[32px] leading-[1.3] text-primary mb-6">{copy.advantage.heading}</h2>
|
||||
<p className="font-body text-lg text-on-surface-variant mb-6">{profile.definitionBlock}</p>
|
||||
<div className="text-xs font-semibold tracking-[0.1em] uppercase text-on-surface-variant">
|
||||
{copy.advantage.updated}: {profile.lastUpdated}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-surface-container rounded-xl p-8 seo-card">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<SeoIcon name="psychology" className="w-8 h-8 text-primary" />
|
||||
<h3 className="font-display font-semibold text-2xl text-primary">{copy.didYouKnow.title}</h3>
|
||||
</div>
|
||||
<ul className="space-y-6">
|
||||
{copy.didYouKnow.items.map((item, index) => (
|
||||
<li key={item.title} className={index < copy.didYouKnow.items.length - 1 ? 'border-b border-outline-variant/30 pb-4' : ''}>
|
||||
<strong className="block font-body text-primary mb-1">{item.title}</strong>
|
||||
<span className="text-sm text-on-surface-variant">{item.body}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export function SeoMethodology({ copy }: { copy: SeoTemplateCopy }) {
|
||||
return (
|
||||
<section className="bg-surface-container-low py-16 md:py-[120px]" id="methodology">
|
||||
<div className="max-w-[1200px] mx-auto px-6">
|
||||
<div className="text-center mb-16">
|
||||
<span className="text-xs font-semibold tracking-[0.1em] uppercase text-primary block mb-4">{copy.methodology.eyebrow}</span>
|
||||
<h2 className="font-display font-semibold text-[32px] md:text-[40px] leading-[1.2] text-primary">{copy.methodology.title}</h2>
|
||||
<p className="font-body text-on-surface-variant max-w-2xl mx-auto mt-4">{copy.methodology.lead}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{copy.methodology.steps.map((step, index) => (
|
||||
<div key={step.title} className="bg-surface p-8 rounded-xl shadow-sm border border-surface-container-high seo-card">
|
||||
<div className="w-12 h-12 bg-primary-container text-on-primary-container rounded-full flex items-center justify-center font-display font-semibold text-xl mb-6">
|
||||
{index + 1}
|
||||
</div>
|
||||
<h3 className="font-display font-semibold text-lg text-primary mb-3">{step.title}</h3>
|
||||
<p className="font-body text-on-surface-variant text-sm">{step.body}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
const comparisonIcons: SeoIconName[] = ['check_circle', 'calendar', 'healing', 'sun']
|
||||
|
||||
export function SeoComparison({ profile, copy }: SectionProps) {
|
||||
const tipLink = profile.relatedLinks[0]
|
||||
|
||||
return (
|
||||
<section className="py-16 md:py-24" id="comparison">
|
||||
<div className="max-w-[1200px] mx-auto px-6 grid grid-cols-1 lg:grid-cols-12 gap-12">
|
||||
<div className="lg:col-span-4">
|
||||
<div className="lg:sticky lg:top-32">
|
||||
<span className="text-xs font-semibold tracking-[0.1em] uppercase text-primary block mb-4">{copy.comparison.eyebrow}</span>
|
||||
<h2 className="font-display font-semibold text-[32px] leading-[1.3] text-primary mb-6">{profile.featureTable.title}</h2>
|
||||
<p className="font-body text-on-surface-variant mb-8">{copy.comparison.lead}</p>
|
||||
{tipLink && (
|
||||
<div className="bg-primary-container/20 p-6 rounded-lg border border-primary-container/30">
|
||||
<h4 className="font-display font-semibold text-base text-primary mb-2">{copy.comparison.tipTitle}</h4>
|
||||
<p className="text-sm text-on-surface-variant mb-3">{copy.comparison.tipBody}</p>
|
||||
<Link className="text-primary text-xs font-semibold tracking-[0.1em] uppercase hover:underline" href={tipLink.href}>
|
||||
{tipLink.label} →
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg:col-span-8 flex flex-col border-t border-surface-container-highest">
|
||||
{profile.featureTable.rows.map((row, index) => (
|
||||
<div key={row.feature} className="grid grid-cols-1 md:grid-cols-2 gap-8 py-8 border-b border-surface-container-highest items-start">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<SeoIcon name={comparisonIcons[index % comparisonIcons.length]} className="w-5 h-5 text-primary" />
|
||||
<h3 className="font-display font-semibold text-lg text-primary">{row.feature}</h3>
|
||||
</div>
|
||||
<div className="text-xs font-semibold tracking-[0.1em] uppercase text-primary mb-2">{copy.comparison.greenlens}</div>
|
||||
<p className="font-body text-on-surface-variant text-sm">{row.greenlens}</p>
|
||||
</div>
|
||||
<div className="md:pt-9">
|
||||
<div className="text-xs font-semibold tracking-[0.1em] uppercase text-on-surface-variant/60 mb-2">
|
||||
{profile.featureTable.alternativeLabel}
|
||||
</div>
|
||||
<p className="font-body text-on-surface-variant/80 text-sm">{row.alternative}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export function SeoContentSections({ profile }: { profile: SeoPageProfile }) {
|
||||
if (!profile.contentSections || profile.contentSections.length === 0) return null
|
||||
|
||||
return (
|
||||
<section className="bg-surface-container-low py-16">
|
||||
<div className="max-w-[1200px] mx-auto px-6 grid grid-cols-1 md:grid-cols-2 gap-8 items-start">
|
||||
{profile.contentSections.map((section) => (
|
||||
<article key={section.title} className="bg-surface p-8 rounded-xl shadow-sm border border-surface-container-high seo-card">
|
||||
<span className="text-xs font-semibold tracking-[0.1em] uppercase text-primary block mb-4">{section.eyebrow}</span>
|
||||
<h2 className="font-display font-semibold text-2xl text-primary mb-4">{section.title}</h2>
|
||||
<p className="font-body text-on-surface-variant text-sm mb-4">{section.body}</p>
|
||||
{section.bullets && (
|
||||
<ul className="space-y-3">
|
||||
{section.bullets.map((item) => (
|
||||
<li key={item} className="flex items-start gap-2 text-sm text-on-surface-variant">
|
||||
<SeoIcon name="check_circle" className="w-4 h-4 mt-0.5 shrink-0 text-primary" />
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export function SeoBestFit({ profile, copy }: SectionProps) {
|
||||
return (
|
||||
<section className="max-w-[1200px] mx-auto px-6 py-16 grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<article className="bg-secondary-container/40 rounded-xl p-8 border border-secondary-container">
|
||||
<span className="text-xs font-semibold tracking-[0.1em] uppercase text-primary block mb-3">{copy.bestFit.chooseTag}</span>
|
||||
<h2 className="font-display font-semibold text-2xl text-primary mb-6">{copy.bestFit.chooseTitle}</h2>
|
||||
<ul className="space-y-4">
|
||||
{profile.greenLensIf.map((item) => (
|
||||
<li key={item} className="flex items-start gap-3 text-sm text-on-surface-variant">
|
||||
<SeoIcon name="check_circle" className="w-5 h-5 mt-0.5 shrink-0 text-primary" />
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
<article className="bg-surface-container rounded-xl p-8 border border-surface-container-high">
|
||||
<span className="text-xs font-semibold tracking-[0.1em] uppercase text-on-surface-variant block mb-3">{copy.bestFit.notTag}</span>
|
||||
<h2 className="font-display font-semibold text-2xl text-primary mb-6">{copy.bestFit.notTitle}</h2>
|
||||
<ul className="space-y-4">
|
||||
{profile.notBestIf.map((item) => (
|
||||
<li key={item} className="flex items-start gap-3 text-sm text-on-surface-variant">
|
||||
<span className="w-5 h-5 mt-0.5 shrink-0 rounded-full border border-outline-variant flex items-center justify-center text-[10px] text-on-surface-variant">
|
||||
–
|
||||
</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export function SeoFaq({ profile, copy }: SectionProps) {
|
||||
return (
|
||||
<section className="bg-surface-container py-16 md:py-24" id="faq">
|
||||
<div className="max-w-[900px] mx-auto px-6">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="font-display font-semibold text-[32px] leading-[1.3] text-primary">{copy.faq.title}</h2>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
{profile.faqs.map((item) => (
|
||||
<div key={item.question} className="bg-surface rounded-lg p-6 shadow-sm">
|
||||
<h3 className="font-display font-semibold text-base text-primary mb-2">{item.question}</h3>
|
||||
<p className="font-body text-on-surface-variant text-sm">{item.answer}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export function SeoRelated({ profile, copy }: SectionProps) {
|
||||
if (profile.relatedLinks.length === 0) return null
|
||||
|
||||
return (
|
||||
<section className="max-w-[1200px] mx-auto px-6 py-16">
|
||||
<div className="mb-10">
|
||||
<span className="text-xs font-semibold tracking-[0.1em] uppercase text-primary block mb-3">{copy.related.tag}</span>
|
||||
<h2 className="font-display font-semibold text-[32px] leading-[1.3] text-primary">{copy.related.title}</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{profile.relatedLinks.map((link) => (
|
||||
<Link key={link.href} href={link.href} className="bg-surface p-6 rounded-xl border border-surface-container-high seo-card block">
|
||||
<h3 className="font-display font-semibold text-lg text-primary mb-2">{link.label}</h3>
|
||||
<p className="text-sm text-on-surface-variant">{link.description}</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user