Compare commits

..

4 Commits

24 changed files with 2022 additions and 345 deletions

View File

@@ -0,0 +1,555 @@
# GreenLens SEO + AI-SEO Roadmap
Stand: 2026-04-10
Quelle:
- Keyword-Datensatz: [keyword-research.csv](C:/Users/a931627/Documents/apps/GreenLns/keyword-research.csv)
- Landing Site: [greenlns-landing](C:/Users/a931627/Documents/apps/GreenLns/greenlns-landing)
## Executive Summary
GreenLens hat genug Nachfrage im Keyword-Set, um eine kleine, sehr fokussierte SEO-Architektur aufzubauen. Das größte Problem ist nicht fehlendes Suchvolumen, sondern fehlende Seitenabdeckung. Die aktuelle Landing Site deckt im Wesentlichen nur Homepage-, Support- und zwei Vergleichsintentionen ab, obwohl die Keyword-Liste starke Nachfrage in vier Kernbereichen zeigt:
1. Plant identification
2. Plant health / diagnosis
3. Plant care / reminders
4. German-language app intent
Zusätzlich gibt es AI-SEO-Potenzial, weil GreenLens bereits mit einem klaren Angle arbeitet:
- plant ER / triage
- next-step diagnosis
- calmer guidance instead of care overload
Dieser Angle ist für AI-Antworten gut verwertbar, wenn die Inhalte als zitierfähige Antwortblöcke statt als generische Marketing-Flächen gebaut werden.
## Phase 0: Fixes Before Content Rollout
Diese Punkte sollten vor dem großen Content-Rollout erledigt werden:
1. Root-canonical aus dem globalen Layout entfernen und pro Seite selbstreferenzierende Canonicals setzen.
2. Keine `hreflang`-Alternates auf `/` ausgeben, solange keine echten Locale-URLs existieren.
3. Für `/privacy`, `/terms` und `/imprint` eigene `metadata` ergänzen.
4. Platzhalter in den Rechtstexten ersetzen.
5. Encoding-/Mojibake-Probleme in sichtbarem Text bereinigen.
6. `Last updated` und Autoren-/Brand-Signale für neue SEO-Seiten einführen.
Ohne diese Vorarbeiten besteht das Risiko, dass neue Seiten schlechter indexiert oder im Snippet-Kontext schwächer interpretiert werden.
## Nachfragebild
### Brutto aus CSV
- Rohsumme: ca. `350.220` bis `3.502.200` Suchen / Monat
### Realistisch dedupliziert
Nach Clusterung ähnlicher Intentionen und konservativem Überlappungsabschlag:
- realistisch: ca. `178.052` bis `1.780.520` Suchen / Monat
- Planungs-Midpoint: ca. `979.286` / Monat
### Interpretation
- Das ist kein Traffic-Forecast.
- Das ist ein adressierbares Suchinteresse aus der vorhandenen Liste.
- Der größte Hebel liegt klar in `plant identifier app`.
- Der zweitgrößte Hebel liegt in diagnosis/symptom content und German app intent.
## Priorisierte Seitenarchitektur
### Wave 1: Highest ROI
#### 1. `/plant-identifier-app`
- Primärkeyword: `plant identifier app`
- Unterstützende Keywords:
- `plant identifier`
- `identify plants by photo`
- `identify plant from picture`
- `plant recognition app`
- `plant id app`
- `free plant identifier app`
- `app to identify plants`
- `ai plant identifier`
- Realistisches Seitenpotenzial: `140.842` bis `1.408.420`
- Planungs-Midpoint: `774.631`
Ziel:
- Haupt-SEO-Landingpage für die Kategorie
- AI-citable Definition und evaluation page
Title:
- `Plant Identifier App for Fast Diagnosis and Care | GreenLens`
Meta Description:
- `GreenLens is a plant identifier app that helps you identify plants by photo, diagnose common plant problems, and get the next best care step in one app.`
H1:
- `Plant Identifier App That Goes Beyond Naming the Plant`
Core outline:
- What is a plant identifier app?
- How GreenLens identifies plants by photo
- Why plant identification alone is not enough
- GreenLens vs generic plant ID apps
- FAQ
AI-SEO answer blocks:
- 40-60 word definition block directly under H1
- short “how it works” numbered list
- table: `GreenLens vs generic plant identifier apps`
- 3-5 symptom-based mini use cases
- FAQ with natural-language questions
Schema:
- `SoftwareApplication`
- `FAQPage`
- optional `HowTo`
Internal links:
- link to `/plant-disease-identifier`
- link to `/plant-care-app`
- link to `/vs/inaturalist`
- link to App Store CTA
Notes:
- This page should be the internal-link hub for the whole organic cluster.
#### 2. `/plant-disease-identifier`
- Primärkeyword: `plant disease identifier`
- Unterstützende Keywords:
- `plant health checker`
- `sick plant diagnosis`
- `plant disease app`
- `plant problem diagnosis`
- `plant health app`
- `pest identification`
- `plant diagnosis app`
- Realistisches Seitenpotenzial: `1.900` bis `19.000`
- Planungs-Midpoint: `10.450`
Ziel:
- Category page for plant diagnosis and symptom-led queries
- Strong AI-overview target because the query is informational and evaluative
Title:
- `Plant Disease Identifier for Houseplant Problems | GreenLens`
Meta Description:
- `Use GreenLens as a plant disease identifier to check common plant problems, understand symptoms, and decide on the next safe care step.`
H1:
- `Plant Disease Identifier for Real-World Plant Problems`
Core outline:
- What a plant disease identifier can and cannot do
- Common symptoms GreenLens helps interpret
- How to avoid wrong next steps
- When a symptom is likely not a disease
- FAQ
AI-SEO answer blocks:
- “What is a plant disease identifier?” answer block
- symptom matrix:
- yellow leaves
- brown leaves
- soft stems
- pest signs
- “most likely cause vs safest next step” table
- FAQ framed around beginner decisions
Schema:
- `FAQPage`
- `HowTo` for diagnosis workflow
Internal links:
- `/plant-identifier-app`
- future `/plant-leaves-turning-yellow`
- future `/brown-leaves-on-houseplants`
#### 3. `/plant-care-app`
- Primärkeyword: `plant care app`
- Unterstützende Keywords:
- `plant care`
- `plant watering reminder`
- `plant watering app`
- `plant care reminder app`
- `houseplant care app`
- `indoor plant care app`
- Realistisches Seitenpotenzial: `1.254` bis `12.540`
- Planungs-Midpoint: `6.897`
Ziel:
- Category page for ongoing care and reminder intent
- Commercial-intent support page that complements diagnosis pages
Title:
- `Plant Care App for Reminders, Routines, and Recovery | GreenLens`
Meta Description:
- `GreenLens is a plant care app for reminders, care routines, plant tracking, and symptom-based next steps when your plant starts to struggle.`
H1:
- `Plant Care App for Better Routines and Better Decisions`
Core outline:
- Why most care apps stop at reminders
- What GreenLens tracks
- Reminder logic vs real plant context
- Care routines for indoor plant owners
- FAQ
AI-SEO answer blocks:
- direct answer: what a plant care app helps with
- feature table: reminders, collection, scan, diagnosis, care notes
- short “when reminders help vs when they hurt” section
Schema:
- `SoftwareApplication`
- `FAQPage`
Internal links:
- `/plant-identifier-app`
- `/plant-disease-identifier`
- future `/plant-tracker-app`
#### 4. `/pflanzen-erkennen-app`
- Primärkeyword: `pflanzen erkennen app`
- Unterstützende Keywords:
- `pflanzenerkennung app`
- `pflanzen bestimmen app`
- `pflanzen app`
- `pflanzen scanner app`
- `pflanzen identifizieren app`
- Realistisches Seitenpotenzial: `1.640` bis `16.400`
- Planungs-Midpoint: `9.020`
Ziel:
- Separate German landing page for German app-intent queries
- Also strong AI-citation candidate for German-language questions
Title:
- `Pflanzen Erkennen App mit Diagnose und Pflegehilfe | GreenLens`
Meta Description:
- `GreenLens ist eine Pflanzen-Erkennen-App, mit der du Pflanzen per Foto bestimmen, Probleme einordnen und die nächsten Pflegeschritte klarer ableiten kannst.`
H1:
- `Pflanzen Erkennen App fuer Fotoerkennung und Pflanzenhilfe`
Core outline:
- Was ist eine Pflanzen-Erkennen-App?
- Pflanzen per Foto bestimmen
- Warum Bestimmung allein nicht reicht
- GreenLens fuer Diagnose und naechste Schritte
- FAQ
AI-SEO answer blocks:
- direkte Antwort auf Deutsch unter dem H1
- “So funktioniert es” als nummerierte Liste
- Vergleichstabelle: `GreenLens vs klassische Pflanzen-Apps`
- FAQ in natuerlicher deutscher Fragesprache
Schema:
- `SoftwareApplication`
- `FAQPage`
Internal links:
- `/plant-identifier-app`
- optional future `/zimmerpflanzen`
- support/legal pages
Notes:
- Diese Seite sollte nicht nur die Homepage uebersetzen, sondern German intent wirklich bedienen.
#### 5. `/vs/inaturalist`
- Primärkeyword: `inaturalist`
- Unterstützende Keywords:
- category fit: alternative / evaluation intent
- Realistisches Seitenpotenzial: `10.000` bis `100.000`
- Planungs-Midpoint: `55.000`
Ziel:
- Comparison page with high citation likelihood in AI answers
- Complements existing `/vs/picturethis` and `/vs/plantum`
Title:
- `GreenLens vs iNaturalist for Plant Identification and Diagnosis`
Meta Description:
- `Compare GreenLens vs iNaturalist for plant identification, plant diagnosis, next-step care guidance, and beginner-friendly decision support.`
H1:
- `GreenLens vs iNaturalist`
Core outline:
- who each product is for
- biodiversity/community app vs plant triage workflow
- identification depth vs next-step diagnosis
- beginner clarity vs expert observation workflow
- FAQ
AI-SEO answer blocks:
- fair comparison summary in first 60 words
- structured comparison table
- “choose GreenLens if / choose iNaturalist if” bullets
- explicit caveat on where iNaturalist is stronger
Schema:
- `FAQPage`
- optional `ItemList`-style structured comparison
Internal links:
- `/plant-identifier-app`
- `/plant-disease-identifier`
- existing comparison pages
### Wave 2: Strong Follow-Up Pages
Diese Seiten haben gute Ergänzungsfunktion oder Long-Tail-/AI-SEO-Wert:
1. `/best-plant-identification-app`
2. `/plant-leaves-turning-yellow`
3. `/brown-leaves-on-houseplants`
4. `/zimmerpflanzen`
5. `/identificador-de-plantas`
6. `/plant-tracker-app`
## Cannibalization Rules
Damit die Seiten sich nicht gegenseitig schwächen:
- `/plant-identifier-app`
- category page
- broad commercial + informational intent
- `/plant-disease-identifier`
- diagnosis-specific category page
- `/plant-care-app`
- routine/reminder/tracking intent
- `/pflanzen-erkennen-app`
- German-language category page
- `/vs/*`
- comparison intent only
- symptom pages
- narrow problem-specific intent only
Regel:
- Jede Seite braucht ein klar eigenes Primärkeyword.
- Das Primärkeyword muss in `title`, `H1`, intro copy, slug und interner Verlinkung konsistent sein.
- Keine zweite Seite sollte dasselbe Keyword-Set als Primärziel bekommen.
## AI-SEO Content Pattern
Alle neuen Pages sollten dieselbe Grundstruktur für AI-Citation verwenden.
### Required above-the-fold structure
1. Direct answer paragraph
2. Clear H1 matching the query
3. 3-bullet summary of when GreenLens is useful
4. Primary CTA
### Required extractable blocks
1. Definition block
- 40-60 words
- answers the primary query directly
2. Comparison block
- table or side-by-side bullets
- especially important for category and alternative pages
3. Decision block
- “Choose GreenLens if...”
- “Not the best fit if...”
4. FAQ block
- 4-6 natural-language questions
- answers should stand alone without surrounding context
5. Freshness block
- visible “Last updated”
- visible review/update cadence
### Recommended AI-citation signals
- specific numbers where they are true and defensible
- product facts in plain language
- one-sentence summary paragraphs
- balanced tone on comparison pages
- author/reviewer attribution
- sources for third-party claims
## Metadata Rules
For all new pages:
- title length target: `50-60` chars where possible
- description target: `140-160` chars
- self-referencing canonical
- Open Graph aligned to title and description
- one H1 only
Template:
```txt
Title: [Primary Keyword] + [specific benefit] | GreenLens
Meta: Clear value proposition with keyword, no fluff, no repetition
H1: Match query closely, but read naturally
```
## Schema Plan
Minimum schema for rollout:
1. Global:
- `Organization`
- `SoftwareApplication`
2. Per page:
- category pages: `FAQPage`
- workflow pages: `HowTo`
- comparison pages: `FAQPage`
Optional later:
- `Review`
- `AggregateRating`
- `BreadcrumbList`
## Internal Linking Plan
### Homepage
Homepage should link prominently to:
- `/plant-identifier-app`
- `/plant-disease-identifier`
- `/plant-care-app`
- `/pflanzen-erkennen-app`
- `/vs/inaturalist`
### Category hub logic
- `/plant-identifier-app` links to all other money pages
- `/plant-disease-identifier` links to symptom pages
- `/plant-care-app` links to tracker/reminder pages
- `/pflanzen-erkennen-app` links to German support cluster
- `/vs/*` links back into category pages
### Anchor text examples
Use varied, natural anchors:
- `plant identifier app`
- `plant disease identifier`
- `plant care app`
- `Pflanzen erkennen App`
- `compare GreenLens and iNaturalist`
Do not overuse exact-match anchors sitewide.
## AI Visibility Monitoring Plan
Test these queries monthly in:
- Google AI Overviews
- ChatGPT search
- Perplexity
### Priority queries
1. `plant identifier app`
2. `identify plants by photo`
3. `plant disease identifier`
4. `plant care app`
5. `best plant identification app`
6. `GreenLens vs PictureThis`
7. `GreenLens vs Plantum`
8. `GreenLens vs iNaturalist`
9. `pflanzen erkennen app`
10. `plant leaves turning yellow`
### Tracking sheet fields
- query
- platform
- AI answer present
- GreenLens cited
- competitor cited
- source page cited
- sentiment / framing
## Off-Site AI-SEO Presence
AI visibility will not come only from GreenLens pages. Parallel actions:
1. Expand comparison page set for major apps in the category.
2. Build review-site presence where relevant.
3. Seek mentions in plant-care roundups and app lists.
4. Create at least one referenceable “best app” style page with a balanced tone.
5. Consider one explainer asset on YouTube for plant diagnosis workflows.
## 30/60/90 Rollout
### First 30 days
1. Fix canonicals, metadata inheritance, legal placeholders, encoding issues.
2. Build:
- `/plant-identifier-app`
- `/plant-disease-identifier`
- `/plant-care-app`
3. Add homepage internal links to these pages.
### Days 31-60
1. Build `/pflanzen-erkennen-app`
2. Build `/vs/inaturalist`
3. Add page-specific schema and update sitemap
4. Start AI visibility checks on top 10 queries
### Days 61-90
1. Build symptom pages:
- `/plant-leaves-turning-yellow`
- `/brown-leaves-on-houseplants`
2. Build `/best-plant-identification-app`
3. Build `/zimmerpflanzen`
4. Review internal links and refresh snippets based on early ranking/citation behavior
## Implementation Notes for This Repo
Recommended file pattern in `greenlns-landing/app`:
- `app/plant-identifier-app/page.tsx`
- `app/plant-disease-identifier/page.tsx`
- `app/plant-care-app/page.tsx`
- `app/pflanzen-erkennen-app/page.tsx`
- `app/vs/inaturalist/page.tsx`
Recommended shared components:
- reusable FAQ component
- reusable comparison table component
- reusable page hero component for category pages
- shared page-level metadata helper
## Recommended First Build Order
If only one wave is built now:
1. `/plant-identifier-app`
2. `/plant-disease-identifier`
3. `/plant-care-app`
4. `/pflanzen-erkennen-app`
5. `/vs/inaturalist`
Reason:
- highest combined SEO + AI-SEO leverage
- strongest match to existing product positioning
- cleanest internal-link structure
- fastest path to broad category coverage

View File

@@ -4,8 +4,6 @@ services:
context: ./greenlns-landing context: ./greenlns-landing
dockerfile: Dockerfile dockerfile: Dockerfile
restart: unless-stopped restart: unless-stopped
ports:
- "3000:3000"
environment: environment:
NODE_ENV: production NODE_ENV: production
PORT: 3000 PORT: 3000
@@ -23,8 +21,6 @@ services:
context: . context: .
dockerfile: server/Dockerfile dockerfile: server/Dockerfile
restart: unless-stopped restart: unless-stopped
ports:
- "3003:3000"
environment: environment:
NODE_ENV: production NODE_ENV: production
PORT: 3000 PORT: 3000
@@ -81,15 +77,12 @@ services:
minio: minio:
image: minio/minio:latest image: minio/minio:latest
restart: unless-stopped restart: unless-stopped
ports:
- "9000:9000"
- "9001:9001"
environment: environment:
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-greenlns-minio} MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-greenlns-minio}
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:?MINIO_SECRET_KEY is required} MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:?MINIO_SECRET_KEY is required}
command: server /data --console-address ":9001" command: server /data --console-address ":9001"
volumes: volumes:
- minio_data:/data - ./minio_data:/data # <-- NEU: Lokaler Ordner statt benanntes Volume!
networks: networks:
- greenlens_net - greenlens_net
healthcheck: healthcheck:
@@ -99,7 +92,6 @@ services:
retries: 5 retries: 5
volumes: volumes:
minio_data:
postgres_data: postgres_data:
networks: networks:

View File

@@ -0,0 +1,71 @@
'use client'
import Link from 'next/link'
import { useLang } from '@/context/LangContext'
import { siteConfig } from '@/lib/site'
const CONTENT = {
de: {
title: 'Impressum',
companyLabel: 'Unternehmen',
addressLabel: 'Adresse',
representativeLabel: 'Vertretungsberechtigt',
contactLabel: 'Kontakt',
registryLabel: 'Register',
vatLabel: 'USt-ID',
},
en: {
title: 'Imprint',
companyLabel: 'Company',
addressLabel: 'Address',
representativeLabel: 'Represented by',
contactLabel: 'Contact',
registryLabel: 'Registry',
vatLabel: 'VAT ID',
},
es: {
title: 'Aviso Legal',
companyLabel: 'Empresa',
addressLabel: 'Direccion',
representativeLabel: 'Representante',
contactLabel: 'Contacto',
registryLabel: 'Registro',
vatLabel: 'IVA',
},
}
export default function ImprintContent() {
const { lang } = useLang()
const c = CONTENT[lang]
return (
<main className="container" style={{ paddingTop: '8rem', paddingBottom: '8rem', maxWidth: '800px' }}>
<p style={{ marginBottom: '1rem', opacity: 0.75 }}>
<Link href="/">Home</Link> / <span>Legal</span> / <span>{c.title}</span>
</p>
<h1>{c.title}</h1>
<div style={{ marginTop: '2rem', lineHeight: '1.8', opacity: 0.9 }}>
<p>
<strong>{c.companyLabel}:</strong> {siteConfig.company.legalName}
</p>
{siteConfig.company.addressLine1 ? (
<p><strong>{c.addressLabel}:</strong> {siteConfig.company.addressLine1}</p>
) : null}
{siteConfig.company.addressLine2 ? <p>{siteConfig.company.addressLine2}</p> : null}
<p>{siteConfig.company.country}</p>
<p>
<strong>{c.representativeLabel}:</strong> {siteConfig.company.representative}
</p>
<p>
<strong>{c.contactLabel}:</strong> <a href={`mailto:${siteConfig.legalEmail}`}>{siteConfig.legalEmail}</a>
</p>
{siteConfig.company.registry ? (
<p><strong>{c.registryLabel}:</strong> {siteConfig.company.registry}</p>
) : null}
{siteConfig.company.vatId ? (
<p><strong>{c.vatLabel}:</strong> {siteConfig.company.vatId}</p>
) : null}
</div>
</main>
)
}

View File

@@ -1,67 +1,37 @@
'use client' import type { Metadata } from 'next'
import Navbar from '@/components/Navbar'
import { useLang } from '@/context/LangContext' import Footer from '@/components/Footer'
import ImprintContent from './ImprintContent'
import { siteConfig } from '@/lib/site' import { siteConfig } from '@/lib/site'
const CONTENT = { const title = 'Imprint'
de: { const description = 'Legal imprint and company contact information for GreenLens.'
title: 'Impressum',
companyLabel: 'Unternehmen', export const metadata: Metadata = {
addressLabel: 'Adresse', title,
representativeLabel: 'Vertretungsberechtigt', description,
contactLabel: 'Kontakt', alternates: {
registryLabel: 'Register', canonical: '/imprint',
vatLabel: 'USt-ID',
}, },
en: { openGraph: {
title: 'Imprint', title: `${title} | GreenLens`,
companyLabel: 'Company', description,
addressLabel: 'Address', url: `${siteConfig.domain}/imprint`,
representativeLabel: 'Represented by', type: 'website',
contactLabel: 'Contact',
registryLabel: 'Registry',
vatLabel: 'VAT ID',
}, },
es: { twitter: {
title: 'Aviso Legal', card: 'summary_large_image',
companyLabel: 'Empresa', title: `${title} | GreenLens`,
addressLabel: 'Direccion', description,
representativeLabel: 'Representante',
contactLabel: 'Contacto',
registryLabel: 'Registro',
vatLabel: 'IVA',
}, },
} }
export default function ImprintPage() { export default function ImprintPage() {
const { lang } = useLang()
const c = CONTENT[lang]
return ( return (
<main className="container" style={{ paddingTop: '8rem', paddingBottom: '8rem', maxWidth: '800px' }}> <>
<h1>{c.title}</h1> <Navbar />
<div style={{ marginTop: '2rem', lineHeight: '1.8', opacity: 0.9 }}> <ImprintContent />
<p> <Footer />
<strong>{c.companyLabel}:</strong> {siteConfig.company.legalName} </>
</p>
{siteConfig.company.addressLine1 ? (
<p><strong>{c.addressLabel}:</strong> {siteConfig.company.addressLine1}</p>
) : null}
{siteConfig.company.addressLine2 ? <p>{siteConfig.company.addressLine2}</p> : null}
<p>{siteConfig.company.country}</p>
<p>
<strong>{c.representativeLabel}:</strong> {siteConfig.company.representative}
</p>
<p>
<strong>{c.contactLabel}:</strong> <a href={`mailto:${siteConfig.legalEmail}`}>{siteConfig.legalEmail}</a>
</p>
{siteConfig.company.registry ? (
<p><strong>{c.registryLabel}:</strong> {siteConfig.company.registry}</p>
) : null}
{siteConfig.company.vatId ? (
<p><strong>{c.vatLabel}:</strong> {siteConfig.company.vatId}</p>
) : null}
</div>
</main>
) )
} }

View File

@@ -29,20 +29,15 @@ export const metadata: Metadata = {
type: 'website', type: 'website',
url: siteConfig.domain, url: siteConfig.domain,
}, },
alternates: {
// Do not emit hreflang until each language has its own URL.
languages: {},
},
twitter: { twitter: {
card: 'summary_large_image', card: 'summary_large_image',
title: 'GreenLens - Plant Identifier and Care Planner', title: 'GreenLens - Plant Identifier and Care Planner',
description: 'Identify plants, get care guidance, and manage your collection with GreenLens.', description: 'Identify plants, get care guidance, and manage your collection with GreenLens.',
}, },
alternates: {
canonical: '/',
languages: {
de: '/',
en: '/',
es: '/',
'x-default': '/',
},
},
} }
export default async function RootLayout({ children }: { children: React.ReactNode }) { export default async function RootLayout({ children }: { children: React.ReactNode }) {

View File

@@ -1,3 +1,4 @@
import type { Metadata } from 'next'
import Navbar from '@/components/Navbar' import Navbar from '@/components/Navbar'
import Hero from '@/components/Hero' import Hero from '@/components/Hero'
import Ticker from '@/components/Ticker' import Ticker from '@/components/Ticker'
@@ -9,6 +10,12 @@ import FAQ from '@/components/FAQ'
import CTA from '@/components/CTA' import CTA from '@/components/CTA'
import Footer from '@/components/Footer' import Footer from '@/components/Footer'
export const metadata: Metadata = {
alternates: {
canonical: '/',
},
}
const howToSchema = { const howToSchema = {
'@context': 'https://schema.org', '@context': 'https://schema.org',
'@type': 'HowTo', '@type': 'HowTo',

View File

@@ -0,0 +1,33 @@
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import SeoCategoryPage from '@/components/SeoCategoryPage'
import { getSeoPageBySlug } from '@/lib/seoPages'
import { siteConfig } from '@/lib/site'
const profile = getSeoPageBySlug('pflanzen-erkennen-app')
export const metadata: Metadata = !profile
? {}
: {
title: profile.metaTitle,
description: profile.metaDescription,
alternates: { canonical: profile.canonical },
openGraph: {
title: profile.metaTitle,
description: profile.metaDescription,
url: `${siteConfig.domain}${profile.canonical}`,
type: 'website',
images: [{ url: '/og-image.png', width: 1200, height: 630, alt: profile.metaTitle }],
},
twitter: {
card: 'summary_large_image',
title: profile.metaTitle,
description: profile.metaDescription,
images: ['/og-image.png'],
},
}
export default function Page() {
if (!profile) notFound()
return <SeoCategoryPage profile={profile} />
}

View File

@@ -0,0 +1,33 @@
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import SeoCategoryPage from '@/components/SeoCategoryPage'
import { getSeoPageBySlug } from '@/lib/seoPages'
import { siteConfig } from '@/lib/site'
const profile = getSeoPageBySlug('plant-care-app')
export const metadata: Metadata = !profile
? {}
: {
title: profile.metaTitle,
description: profile.metaDescription,
alternates: { canonical: profile.canonical },
openGraph: {
title: profile.metaTitle,
description: profile.metaDescription,
url: `${siteConfig.domain}${profile.canonical}`,
type: 'website',
images: [{ url: '/og-image.png', width: 1200, height: 630, alt: profile.metaTitle }],
},
twitter: {
card: 'summary_large_image',
title: profile.metaTitle,
description: profile.metaDescription,
images: ['/og-image.png'],
},
}
export default function Page() {
if (!profile) notFound()
return <SeoCategoryPage profile={profile} />
}

View File

@@ -0,0 +1,33 @@
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import SeoCategoryPage from '@/components/SeoCategoryPage'
import { getSeoPageBySlug } from '@/lib/seoPages'
import { siteConfig } from '@/lib/site'
const profile = getSeoPageBySlug('plant-disease-identifier')
export const metadata: Metadata = !profile
? {}
: {
title: profile.metaTitle,
description: profile.metaDescription,
alternates: { canonical: profile.canonical },
openGraph: {
title: profile.metaTitle,
description: profile.metaDescription,
url: `${siteConfig.domain}${profile.canonical}`,
type: 'website',
images: [{ url: '/og-image.png', width: 1200, height: 630, alt: profile.metaTitle }],
},
twitter: {
card: 'summary_large_image',
title: profile.metaTitle,
description: profile.metaDescription,
images: ['/og-image.png'],
},
}
export default function Page() {
if (!profile) notFound()
return <SeoCategoryPage profile={profile} />
}

View File

@@ -0,0 +1,33 @@
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import SeoCategoryPage from '@/components/SeoCategoryPage'
import { getSeoPageBySlug } from '@/lib/seoPages'
import { siteConfig } from '@/lib/site'
const profile = getSeoPageBySlug('plant-identifier-app')
export const metadata: Metadata = !profile
? {}
: {
title: profile.metaTitle,
description: profile.metaDescription,
alternates: { canonical: profile.canonical },
openGraph: {
title: profile.metaTitle,
description: profile.metaDescription,
url: `${siteConfig.domain}${profile.canonical}`,
type: 'website',
images: [{ url: '/og-image.png', width: 1200, height: 630, alt: profile.metaTitle }],
},
twitter: {
card: 'summary_large_image',
title: profile.metaTitle,
description: profile.metaDescription,
images: ['/og-image.png'],
},
}
export default function Page() {
if (!profile) notFound()
return <SeoCategoryPage profile={profile} />
}

View File

@@ -0,0 +1,84 @@
'use client'
import Link from 'next/link'
import { useLang } from '@/context/LangContext'
import { siteConfig } from '@/lib/site'
const CONTENT = {
de: {
title: 'Datenschutzerklaerung',
intro:
'Der Schutz personenbezogener Daten ist uns wichtig. Diese Seite beschreibt in knapper Form, welche Daten GreenLens verarbeiten kann und warum.',
section1: '1. Verarbeitete Daten',
text1:
'Je nach Nutzung der App koennen Pflanzenfotos, Kontodaten, technische Geraeteinformationen, In-App-Kaufdaten sowie freiwillig uebermittelte Support-Anfragen verarbeitet werden.',
section2: '2. Zweck der Verarbeitung',
text2:
'Diese Daten werden verwendet, um Pflanzenscans auszufuehren, deine Sammlung zu speichern, Erinnerungen bereitzustellen, Abos und Credits zu verwalten sowie Support-Anfragen zu beantworten.',
section3: '3. Drittanbieter',
text3:
'GreenLens nutzt technische Dienstleister fuer App-Betrieb, Analyse, Authentifizierung und In-App-Kaeufe. Dazu koennen je nach Plattform Apple, RevenueCat, PostHog oder Hosting-Anbieter gehoeren.',
section4: '4. Kontakt',
text4: 'Bei Fragen zum Datenschutz oder zu deinen Datenrechten kannst du uns per E-Mail kontaktieren.',
},
en: {
title: 'Privacy Policy',
intro:
'Protecting personal data matters to us. This page summarizes what GreenLens may process and why.',
section1: '1. Data we may process',
text1:
'Depending on how you use the app, GreenLens may process plant photos, account details, technical device information, in-app purchase data, and support messages you send to us.',
section2: '2. Why we process it',
text2:
'We use this information to run plant scans, store your collection, provide reminders, manage subscriptions and credits, and respond to support requests.',
section3: '3. Third-party services',
text3:
'GreenLens uses service providers for app delivery, analytics, authentication, and in-app purchases. Depending on platform and setup, this can include Apple, RevenueCat, PostHog, or hosting providers.',
section4: '4. Contact',
text4: 'If you have privacy questions or want to exercise your data rights, contact us by email.',
},
es: {
title: 'Politica de Privacidad',
intro:
'La proteccion de los datos personales es importante para nosotros. Esta pagina resume que datos puede procesar GreenLens y por que.',
section1: '1. Datos que podemos procesar',
text1:
'Segun el uso de la app, GreenLens puede procesar fotos de plantas, datos de cuenta, informacion tecnica del dispositivo, datos de compras dentro de la app y mensajes de soporte.',
section2: '2. Para que los usamos',
text2:
'Usamos estos datos para ejecutar escaneos, guardar tu coleccion, ofrecer recordatorios, gestionar suscripciones y creditos, y responder solicitudes de soporte.',
section3: '3. Servicios de terceros',
text3:
'GreenLens utiliza proveedores para la operacion de la app, analitica, autenticacion y compras integradas. Segun la plataforma, esto puede incluir Apple, RevenueCat, PostHog o proveedores de hosting.',
section4: '4. Contacto',
text4: 'Si tienes preguntas sobre privacidad o tus derechos de datos, contactanos por correo electronico.',
},
}
export default function PrivacyContent() {
const { lang } = useLang()
const c = CONTENT[lang]
return (
<main className="container" style={{ paddingTop: '8rem', paddingBottom: '8rem', maxWidth: '800px' }}>
<p style={{ marginBottom: '1rem', opacity: 0.75 }}>
<Link href="/">Home</Link> / <span>Legal</span> / <span>{c.title}</span>
</p>
<h1>{c.title}</h1>
<div style={{ marginTop: '2rem', lineHeight: '1.8', opacity: 0.9 }}>
<p>{c.intro}</p>
<h2 style={{ marginTop: '1.5rem', fontSize: '1.25rem' }}>{c.section1}</h2>
<p>{c.text1}</p>
<h2 style={{ marginTop: '1.5rem', fontSize: '1.25rem' }}>{c.section2}</h2>
<p>{c.text2}</p>
<h2 style={{ marginTop: '1.5rem', fontSize: '1.25rem' }}>{c.section3}</h2>
<p>{c.text3}</p>
<h2 style={{ marginTop: '1.5rem', fontSize: '1.25rem' }}>{c.section4}</h2>
<p>{c.text4}</p>
<p style={{ marginTop: '0.75rem' }}>
<a href={`mailto:${siteConfig.legalEmail}`}>{siteConfig.legalEmail}</a>
</p>
</div>
</main>
)
}

View File

@@ -1,80 +1,37 @@
'use client' import type { Metadata } from 'next'
import Navbar from '@/components/Navbar'
import { useLang } from '@/context/LangContext' import Footer from '@/components/Footer'
import PrivacyContent from './PrivacyContent'
import { siteConfig } from '@/lib/site' import { siteConfig } from '@/lib/site'
const CONTENT = { const title = 'Privacy Policy'
de: { const description = 'Learn what personal data GreenLens processes, why it is used, and how to contact us about privacy.'
title: 'Datenschutzerklaerung',
intro: export const metadata: Metadata = {
'Der Schutz personenbezogener Daten ist uns wichtig. Diese Seite beschreibt in knapper Form, welche Daten GreenLens verarbeiten kann und warum.', title,
section1: '1. Verarbeitete Daten', description,
text1: alternates: {
'Je nach Nutzung der App koennen Pflanzenfotos, Kontodaten, technische Geraeteinformationen, In-App-Kaufdaten sowie freiwillig uebermittelte Support-Anfragen verarbeitet werden.', canonical: '/privacy',
section2: '2. Zweck der Verarbeitung',
text2:
'Diese Daten werden verwendet, um Pflanzenscans auszufuehren, deine Sammlung zu speichern, Erinnerungen bereitzustellen, Abos und Credits zu verwalten sowie Support-Anfragen zu beantworten.',
section3: '3. Drittanbieter',
text3:
'GreenLens nutzt technische Dienstleister fuer App-Betrieb, Analyse, Authentifizierung und In-App-Kaeufe. Dazu koennen je nach Plattform Apple, RevenueCat, PostHog oder Hosting-Anbieter gehoeren.',
section4: '4. Kontakt',
text4: 'Bei Fragen zum Datenschutz oder zu deinen Datenrechten kannst du uns per E-Mail kontaktieren.',
}, },
en: { openGraph: {
title: 'Privacy Policy', title: `${title} | GreenLens`,
intro: description,
'Protecting personal data matters to us. This page summarizes what GreenLens may process and why.', url: `${siteConfig.domain}/privacy`,
section1: '1. Data we may process', type: 'website',
text1:
'Depending on how you use the app, GreenLens may process plant photos, account details, technical device information, in-app purchase data, and support messages you send to us.',
section2: '2. Why we process it',
text2:
'We use this information to run plant scans, store your collection, provide reminders, manage subscriptions and credits, and respond to support requests.',
section3: '3. Third-party services',
text3:
'GreenLens uses service providers for app delivery, analytics, authentication, and in-app purchases. Depending on platform and setup, this can include Apple, RevenueCat, PostHog, or hosting providers.',
section4: '4. Contact',
text4: 'If you have privacy questions or want to exercise your data rights, contact us by email.',
}, },
es: { twitter: {
title: 'Politica de Privacidad', card: 'summary_large_image',
intro: title: `${title} | GreenLens`,
'La proteccion de los datos personales es importante para nosotros. Esta pagina resume que datos puede procesar GreenLens y por que.', description,
section1: '1. Datos que podemos procesar',
text1:
'Segun el uso de la app, GreenLens puede procesar fotos de plantas, datos de cuenta, informacion tecnica del dispositivo, datos de compras dentro de la app y mensajes de soporte.',
section2: '2. Para que los usamos',
text2:
'Usamos estos datos para ejecutar escaneos, guardar tu coleccion, ofrecer recordatorios, gestionar suscripciones y creditos, y responder solicitudes de soporte.',
section3: '3. Servicios de terceros',
text3:
'GreenLens utiliza proveedores para la operacion de la app, analitica, autenticacion y compras integradas. Segun la plataforma, esto puede incluir Apple, RevenueCat, PostHog o proveedores de hosting.',
section4: '4. Contacto',
text4: 'Si tienes preguntas sobre privacidad o tus derechos de datos, contactanos por correo electronico.',
}, },
} }
export default function PrivacyPage() { export default function PrivacyPage() {
const { lang } = useLang()
const c = CONTENT[lang]
return ( return (
<main className="container" style={{ paddingTop: '8rem', paddingBottom: '8rem', maxWidth: '800px' }}> <>
<h1>{c.title}</h1> <Navbar />
<div style={{ marginTop: '2rem', lineHeight: '1.8', opacity: 0.9 }}> <PrivacyContent />
<p>{c.intro}</p> <Footer />
<h2 style={{ marginTop: '1.5rem', fontSize: '1.25rem' }}>{c.section1}</h2> </>
<p>{c.text1}</p>
<h2 style={{ marginTop: '1.5rem', fontSize: '1.25rem' }}>{c.section2}</h2>
<p>{c.text2}</p>
<h2 style={{ marginTop: '1.5rem', fontSize: '1.25rem' }}>{c.section3}</h2>
<p>{c.text3}</p>
<h2 style={{ marginTop: '1.5rem', fontSize: '1.25rem' }}>{c.section4}</h2>
<p>{c.text4}</p>
<p style={{ marginTop: '0.75rem' }}>
<a href={`mailto:${siteConfig.legalEmail}`}>{siteConfig.legalEmail}</a>
</p>
</div>
</main>
) )
} }

View File

@@ -16,6 +16,30 @@ export default function sitemap(): MetadataRoute.Sitemap {
changeFrequency: 'monthly', changeFrequency: 'monthly',
priority: 0.5, priority: 0.5,
}, },
{
url: `${baseUrl}/plant-identifier-app`,
lastModified: new Date('2026-04-12'),
changeFrequency: 'monthly',
priority: 0.8,
},
{
url: `${baseUrl}/plant-disease-identifier`,
lastModified: new Date('2026-04-12'),
changeFrequency: 'monthly',
priority: 0.75,
},
{
url: `${baseUrl}/plant-care-app`,
lastModified: new Date('2026-04-12'),
changeFrequency: 'monthly',
priority: 0.75,
},
{
url: `${baseUrl}/pflanzen-erkennen-app`,
lastModified: new Date('2026-04-12'),
changeFrequency: 'monthly',
priority: 0.75,
},
{ {
url: `${baseUrl}/vs/picturethis`, url: `${baseUrl}/vs/picturethis`,
lastModified: new Date('2026-04-10'), lastModified: new Date('2026-04-10'),
@@ -28,6 +52,12 @@ export default function sitemap(): MetadataRoute.Sitemap {
changeFrequency: 'monthly', changeFrequency: 'monthly',
priority: 0.65, priority: 0.65,
}, },
{
url: `${baseUrl}/vs/inaturalist`,
lastModified: new Date('2026-04-12'),
changeFrequency: 'monthly',
priority: 0.65,
},
{ {
url: `${baseUrl}/imprint`, url: `${baseUrl}/imprint`,
lastModified: new Date('2026-04-08'), lastModified: new Date('2026-04-08'),

View File

@@ -1,10 +1,26 @@
import type { Metadata } from 'next' import type { Metadata } from 'next'
import Link from 'next/link' import Link from 'next/link'
import Navbar from '@/components/Navbar'
import Footer from '@/components/Footer'
import { siteConfig } from '@/lib/site' import { siteConfig } from '@/lib/site'
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Support', title: 'Support',
description: 'Get support for GreenLens, including contact details, onboarding help, billing guidance, and privacy links.', description: 'Get support for GreenLens, including contact details, onboarding help, billing guidance, and privacy links.',
alternates: {
canonical: '/support',
},
openGraph: {
title: 'Support | GreenLens',
description: 'Get support for GreenLens, including contact details, onboarding help, billing guidance, and privacy links.',
url: `${siteConfig.domain}/support`,
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'Support | GreenLens',
description: 'Get support for GreenLens, including contact details, onboarding help, billing guidance, and privacy links.',
},
} }
const faqs = [ const faqs = [
@@ -32,73 +48,81 @@ const faqs = [
export default function SupportPage() { export default function SupportPage() {
return ( return (
<main className="support-page"> <>
<section className="support-hero"> <Navbar />
<div className="container support-hero-inner"> <main className="support-page">
<p className="tag">Support</p> <section className="support-hero">
<h1>Help for scans, care plans, billing, and account questions.</h1> <div className="container support-hero-inner">
<p className="support-lead"> <p style={{ marginBottom: '1rem', opacity: 0.75 }}>
GreenLens helps users identify plants, understand their condition, and keep a collection organized. <Link href="/">Home</Link> / <span>Support</span>
If something breaks or feels unclear, this is the fastest place to start.
</p>
<div className="support-actions">
<a className="btn-primary" href={`mailto:${siteConfig.supportEmail}`}>
Email Support
</a>
<Link className="btn-outline support-outline" href="/privacy">
Privacy Policy
</Link>
</div>
</div>
</section>
<section className="support-grid-wrap">
<div className="container support-grid">
<div className="support-card">
<h2>Contact</h2>
<p>
Email us at <a href={`mailto:${siteConfig.supportEmail}`}>{siteConfig.supportEmail}</a>
</p> </p>
<p>Include your device type, app version, and what happened right before the issue.</p> <p className="tag">Support</p>
</div> <h1>Help for scans, care plans, billing, and account questions.</h1>
<p className="support-lead">
<div className="support-card"> GreenLens helps users identify plants, understand their condition, and keep a collection organized.
<h2>Common topics</h2> If something breaks or feels unclear, this is the fastest place to start.
<ul className="support-list">
<li>Plant identification issues</li>
<li>Care reminder questions</li>
<li>Subscriptions and credit purchases</li>
<li>Account access and saved data</li>
</ul>
</div>
<div className="support-card">
<h2>Legal</h2>
<p>
Review our <Link href="/privacy">Privacy Policy</Link> and <Link href="/imprint">Imprint</Link>.
</p> </p>
<p>These links should be used in App Store Connect before submission.</p> <div className="support-actions">
<a className="btn-primary" href={`mailto:${siteConfig.supportEmail}`}>
Email Support
</a>
<Link className="btn-outline support-outline" href="/privacy">
Privacy Policy
</Link>
</div>
</div> </div>
</div> </section>
</section>
<section className="support-faq"> <section className="support-grid-wrap">
<div className="container"> <div className="container support-grid">
<div className="support-section-head"> <div className="support-card">
<p className="tag">FAQ</p> <h2>Contact</h2>
<h2>Quick answers before you write support.</h2> <p>
</div> Email us at <a href={`mailto:${siteConfig.supportEmail}`}>{siteConfig.supportEmail}</a>
</p>
<p>Include your device type, app version, and what happened right before the issue.</p>
</div>
<div className="support-faq-list"> <div className="support-card">
{faqs.map((item) => ( <h2>Common topics</h2>
<article key={item.question} className="support-faq-item"> <ul className="support-list">
<h3>{item.question}</h3> <li>Plant identification issues</li>
<p>{item.answer}</p> <li>Care reminder questions</li>
</article> <li>Subscriptions and credit purchases</li>
))} <li>Account access and saved data</li>
</ul>
</div>
<div className="support-card">
<h2>Legal</h2>
<p>
Review our <Link href="/privacy">Privacy Policy</Link>, <Link href="/terms">Terms of Service</Link>,
and <Link href="/imprint">Imprint</Link>.
</p>
<p>These links should be used in App Store Connect before submission.</p>
</div>
</div> </div>
</div> </section>
</section>
</main> <section className="support-faq">
<div className="container">
<div className="support-section-head">
<p className="tag">FAQ</p>
<h2>Quick answers before you write support.</h2>
</div>
<div className="support-faq-list">
{faqs.map((item) => (
<article key={item.question} className="support-faq-item">
<h3>{item.question}</h3>
<p>{item.answer}</p>
</article>
))}
</div>
</div>
</section>
</main>
<Footer />
</>
) )
} }

View File

@@ -0,0 +1,51 @@
'use client'
import Link from 'next/link'
import { useLang } from '@/context/LangContext'
import { siteConfig } from '@/lib/site'
const CONTENT = {
de: {
title: 'Nutzungsbedingungen',
intro: 'Diese Bedingungen regeln die Nutzung von GreenLens und der dazugehoerigen Services.',
section1: 'GreenLens wird als digitale App und Web-Service fuer Pflanzenscans, Informationen und accountbezogene Funktionen bereitgestellt.',
section2: 'Vor dem Livegang muessen diese Bedingungen durch rechtlich gepruefte und vollstaendige Vertragstexte ersetzt werden.',
contactLabel: 'Kontakt',
},
en: {
title: 'Terms of Service',
intro: 'These terms govern the use of GreenLens and its related services.',
section1: 'GreenLens is provided as a digital app and web service for plant scans, information, and account-related functionality.',
section2: 'Before launch, replace this placeholder with legally reviewed and complete terms for your business.',
contactLabel: 'Contact',
},
es: {
title: 'Terminos del Servicio',
intro: 'Estos terminos regulan el uso de GreenLens y sus servicios relacionados.',
section1: 'GreenLens se ofrece como app y servicio web para escaneo de plantas, informacion y funciones de cuenta.',
section2: 'Antes del lanzamiento, sustituye este texto por terminos completos revisados legalmente.',
contactLabel: 'Contacto',
},
}
export default function TermsContent() {
const { lang } = useLang()
const c = CONTENT[lang]
return (
<main className="container" style={{ paddingTop: '8rem', paddingBottom: '8rem', maxWidth: '800px' }}>
<p style={{ marginBottom: '1rem', opacity: 0.75 }}>
<Link href="/">Home</Link> / <span>Legal</span> / <span>{c.title}</span>
</p>
<h1>{c.title}</h1>
<div style={{ marginTop: '2rem', lineHeight: '1.8', opacity: 0.9 }}>
<p>{c.intro}</p>
<p>{c.section1}</p>
<p>{c.section2}</p>
<p>
<strong>{c.contactLabel}:</strong> <a href={`mailto:${siteConfig.legalEmail}`}>{siteConfig.legalEmail}</a>
</p>
</div>
</main>
)
}

View File

@@ -1,47 +1,37 @@
'use client' import type { Metadata } from 'next'
import Navbar from '@/components/Navbar'
import { useLang } from '@/context/LangContext' import Footer from '@/components/Footer'
import TermsContent from './TermsContent'
import { siteConfig } from '@/lib/site' import { siteConfig } from '@/lib/site'
const CONTENT = { const title = 'Terms of Service'
de: { const description = 'Review the current GreenLens terms governing use of the app and related services.'
title: 'Nutzungsbedingungen',
intro: 'Diese Bedingungen regeln die Nutzung von GreenLens und der dazugehoerigen Services.', export const metadata: Metadata = {
section1: 'GreenLens wird als digitale App und Web-Service fuer Pflanzenscans, Informationen und accountbezogene Funktionen bereitgestellt.', title,
section2: 'Vor dem Livegang muessen diese Bedingungen durch rechtlich gepruefte und vollstaendige Vertragstexte ersetzt werden.', description,
contactLabel: 'Kontakt', alternates: {
canonical: '/terms',
}, },
en: { openGraph: {
title: 'Terms of Service', title: `${title} | GreenLens`,
intro: 'These terms govern the use of GreenLens and its related services.', description,
section1: 'GreenLens is provided as a digital app and web service for plant scans, information, and account-related functionality.', url: `${siteConfig.domain}/terms`,
section2: 'Before launch, replace this placeholder with legally reviewed and complete terms for your business.', type: 'website',
contactLabel: 'Contact',
}, },
es: { twitter: {
title: 'Terminos del Servicio', card: 'summary_large_image',
intro: 'Estos terminos regulan el uso de GreenLens y sus servicios relacionados.', title: `${title} | GreenLens`,
section1: 'GreenLens se ofrece como app y servicio web para escaneo de plantas, informacion y funciones de cuenta.', description,
section2: 'Antes del lanzamiento, sustituye este texto por terminos completos revisados legalmente.',
contactLabel: 'Contacto',
}, },
} }
export default function TermsPage() { export default function TermsPage() {
const { lang } = useLang()
const c = CONTENT[lang]
return ( return (
<main className="container" style={{ paddingTop: '8rem', paddingBottom: '8rem', maxWidth: '800px' }}> <>
<h1>{c.title}</h1> <Navbar />
<div style={{ marginTop: '2rem', lineHeight: '1.8', opacity: 0.9 }}> <TermsContent />
<p>{c.intro}</p> <Footer />
<p>{c.section1}</p> </>
<p>{c.section2}</p>
<p>
<strong>{c.contactLabel}:</strong> <a href={`mailto:${siteConfig.legalEmail}`}>{siteConfig.legalEmail}</a>
</p>
</div>
</main>
) )
} }

View File

@@ -38,7 +38,7 @@ export default function CTA() {
</div> </div>
</a> </a>
<a <a
href={hasAndroidStoreUrl ? siteConfig.androidPlayStoreUrl : `mailto:${siteConfig.supportEmail}`} href={hasAndroidStoreUrl ? siteConfig.androidPlayStoreUrl : '/support'}
className="store-btn" className="store-btn"
id="cta-googleplay" id="cta-googleplay"
aria-label="Google Play or contact" aria-label="Google Play or contact"

View File

@@ -1,17 +1,19 @@
'use client' 'use client'
import Link from 'next/link' import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useLang } from '@/context/LangContext' import { useLang } from '@/context/LangContext'
import { siteConfig } from '@/lib/site' import { siteConfig } from '@/lib/site'
const LINK_HREFS = [
['#features', '#intelligence', '#cta', '/support'],
['/#how', '/#faq', '/support'],
['/imprint', '/privacy'],
]
export default function Footer() { export default function Footer() {
const pathname = usePathname()
const { t } = useLang() const { t } = useLang()
const homeHref = (hash: string) => (pathname === '/' ? hash : `/${hash}`)
const linkHrefs = [
[homeHref('#features'), homeHref('#intelligence'), homeHref('#cta'), '/support'],
[homeHref('#how'), homeHref('#faq'), '/support'],
['/imprint', '/privacy', '/terms'],
]
return ( return (
<footer className="footer" id="footer"> <footer className="footer" id="footer">
@@ -28,14 +30,19 @@ export default function Footer() {
<div className="footer-col" key={col.title}> <div className="footer-col" key={col.title}>
<div className="footer-col-title">{col.title}</div> <div className="footer-col-title">{col.title}</div>
{col.links.map((label, li) => ( {col.links.map((label, li) => (
<Link key={label} href={LINK_HREFS[ci]?.[li] ?? '/support'}> <Link key={label} href={linkHrefs[ci]?.[li] ?? '/support'}>
{label} {label}
</Link> </Link>
))} ))}
{ci === 1 && ( {ci === 1 && (
<> <>
<Link href="/plant-identifier-app">Plant Identifier App</Link>
<Link href="/plant-disease-identifier">Plant Disease Identifier</Link>
<Link href="/plant-care-app">Plant Care App</Link>
<Link href="/pflanzen-erkennen-app">Pflanzen erkennen</Link>
<Link href="/vs/picturethis">GreenLens vs PictureThis</Link> <Link href="/vs/picturethis">GreenLens vs PictureThis</Link>
<Link href="/vs/plantum">GreenLens vs Plantum</Link> <Link href="/vs/plantum">GreenLens vs Plantum</Link>
<Link href="/vs/inaturalist">GreenLens vs iNaturalist</Link>
</> </>
)} )}
</div> </div>
@@ -46,9 +53,9 @@ export default function Footer() {
<div className="footer-bottom"> <div className="footer-bottom">
<p>{t.footer.copy}</p> <p>{t.footer.copy}</p>
<a href={`mailto:${siteConfig.supportEmail}`} className="footer-contact"> <Link href="/support" className="footer-contact">
{siteConfig.supportEmail} Support
</a> </Link>
</div> </div>
</div> </div>
</footer> </footer>

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import Link from 'next/link' import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useLang } from '@/context/LangContext' import { useLang } from '@/context/LangContext'
import type { Lang } from '@/lib/i18n' import type { Lang } from '@/lib/i18n'
@@ -14,7 +15,9 @@ const LANGS: { code: Lang; label: string; flag: string }[] = [
export default function Navbar() { export default function Navbar() {
const [scrolled, setScrolled] = useState(false) const [scrolled, setScrolled] = useState(false)
const [menuOpen, setMenuOpen] = useState(false) const [menuOpen, setMenuOpen] = useState(false)
const pathname = usePathname()
const { lang, setLang, t } = useLang() const { lang, setLang, t } = useLang()
const homeHref = (hash: string) => (pathname === '/' ? hash : `/${hash}`)
useEffect(() => { useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 40) const onScroll = () => setScrolled(window.scrollY > 40)
@@ -30,12 +33,12 @@ export default function Navbar() {
</Link> </Link>
<div className={`nav-links${menuOpen ? ' nav-links--open' : ''}`}> <div className={`nav-links${menuOpen ? ' nav-links--open' : ''}`}>
<a href="#features" onClick={() => setMenuOpen(false)}>{t.nav.features}</a> <a href={homeHref('#features')} onClick={() => setMenuOpen(false)}>{t.nav.features}</a>
<a href="#intelligence" onClick={() => setMenuOpen(false)}>{t.nav.tech}</a> <a href={homeHref('#intelligence')} onClick={() => setMenuOpen(false)}>{t.nav.tech}</a>
<a href="#faq" onClick={() => setMenuOpen(false)}>FAQ</a> <a href={homeHref('#faq')} onClick={() => setMenuOpen(false)}>FAQ</a>
<a href="#how" onClick={() => setMenuOpen(false)}>{t.nav.how}</a> <a href={homeHref('#how')} onClick={() => setMenuOpen(false)}>{t.nav.how}</a>
<Link href="/support" onClick={() => setMenuOpen(false)}>Support</Link> <Link href="/support" onClick={() => setMenuOpen(false)}>Support</Link>
<a href="#cta" onClick={() => setMenuOpen(false)}>{t.nav.download}</a> <a href={homeHref('#cta')} onClick={() => setMenuOpen(false)}>{t.nav.download}</a>
<div className="lang-switcher" role="group" aria-label="Language selector"> <div className="lang-switcher" role="group" aria-label="Language selector">
{LANGS.map((l) => ( {LANGS.map((l) => (
@@ -55,7 +58,7 @@ export default function Navbar() {
))} ))}
</div> </div>
<a href="#cta" className="nav-cta" onClick={() => setMenuOpen(false)}>{t.nav.cta}</a> <a href={homeHref('#cta')} className="nav-cta" onClick={() => setMenuOpen(false)}>{t.nav.cta}</a>
</div> </div>
<button <button

View File

@@ -0,0 +1,177 @@
import Link from 'next/link'
import Navbar from '@/components/Navbar'
import CTA from '@/components/CTA'
import Footer from '@/components/Footer'
import type { SeoPageProfile } from '@/lib/seoPages'
import { siteConfig, hasIosStoreUrl } from '@/lib/site'
interface SeoCategoryPageProps {
profile: SeoPageProfile
}
export default function SeoCategoryPage({ profile }: SeoCategoryPageProps) {
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, Android',
applicationCategory: 'LifestyleApplication',
description: profile.directAnswer,
...(hasIosStoreUrl && { downloadUrl: siteConfig.iosAppStoreUrl }),
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'EUR',
},
}
: null
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqSchema) }}
/>
{appSchema && (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(appSchema) }}
/>
)}
<Navbar />
<main className="comparison-page">
{/* Hero */}
<section className="comparison-hero">
<div className="container comparison-hero-grid">
<div className="comparison-hero-copy">
<p className="tag">GreenLens</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">Try GreenLens</a>
<a href="#feature-table" className="btn-outline">See full comparison</a>
</div>
</div>
<aside className="comparison-hero-card">
<p className="comparison-card-label">Definition</p>
<p>{profile.definitionBlock}</p>
<p className="comparison-verified">Last updated: {profile.lastUpdated}</p>
</aside>
</div>
</section>
{/* Feature table */}
<section className="comparison-table-section" id="feature-table">
<div className="container">
<div className="comparison-section-head">
<p className="tag">At a glance</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">Best fit</p>
<h2>Choose GreenLens if:</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">Not the best fit</p>
<h2>GreenLens is not the right tool if:</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">FAQ</p>
<h2>Common questions answered directly.</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">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">Need help?</p>
<h3>Talk to GreenLens support</h3>
<p>
Questions about scans, care plans, billing, or features? Use the support page.
</p>
</Link>
</div>
</section>
)}
<CTA />
</main>
<Footer />
</>
)
}

View File

@@ -1,4 +1,4 @@
export type CompetitorSlug = 'picturethis' | 'plantum' export type CompetitorSlug = 'picturethis' | 'plantum' | 'inaturalist'
export interface ComparisonThesis { export interface ComparisonThesis {
title: string title: string
@@ -365,12 +365,172 @@ export const competitorProfiles: Record<CompetitorSlug, CompetitorProfile> = {
}, },
], ],
}, },
inaturalist: {
slug: 'inaturalist',
name: 'iNaturalist',
metaTitle: 'GreenLens vs iNaturalist',
metaDescription:
'Compare GreenLens vs iNaturalist for plant identification, care guidance, and disease triage. See which app fits your situation — owned plant care or biodiversity discovery.',
heroSummary:
'iNaturalist is one of the most respected citizen science platforms in the world. GreenLens is built for a different job: helping you decide what to do next when a plant you own is struggling. These two tools solve different problems, and the right choice depends entirely on what you are trying to accomplish.',
heroVerdict: [
'Choose GreenLens if you own the plant and need the next actionable step — care guidance, symptom triage, or rescue decisions.',
'Choose iNaturalist if your goal is species discovery, community identification, or contributing to biodiversity research.',
'The comparison only becomes interesting when someone wants both. For plant owners in triage mode, GreenLens is the faster path.',
],
disclaimer:
'iNaturalist is a non-profit platform. This comparison is based on public features and user-reported use cases. GreenLens and iNaturalist serve different primary audiences.',
lastVerified: 'April 2026',
competitorSnapshot:
'iNaturalist is a global biodiversity mapping platform backed by the California Academy of Sciences and National Geographic. It is primarily a citizen science tool: users upload observations, receive community-sourced identifications, and contribute to scientific datasets. Its plant identification is broad and accurate, but the platform is not designed around plant ownership, care routines, or emergency triage.',
greenLensPositioning:
'GreenLens is an owned-plant companion. It assumes you are already responsible for the plant and need help deciding what to do next — whether that is diagnosis, care scheduling, or understanding why a leaf is turning yellow.',
whyPeopleCompare: [
'They found iNaturalist useful for ID but now need care guidance the platform does not provide.',
'They want a free identification option and are evaluating whether iNaturalist is enough.',
'They are confused by whether a citizen science app can replace a dedicated plant care app.',
],
theses: [
{
title: 'Community ID vs owned-plant triage',
greenlens:
'GreenLens assumes you own the plant and need a next step. Diagnosis, care context, and actionable decisions are built into the core workflow.',
competitor:
'iNaturalist crowdsources identifications from a global community. The platform is optimized for observation accuracy, not for what you should do once you know the species.',
},
{
title: 'App-first speed vs community dependence',
greenlens:
'GreenLens returns AI-driven results instantly, without waiting for community votes or reviews.',
competitor:
'iNaturalist offers instant AI suggestions at upload, but expert community confirmation — the step that makes an observation Research Grade — can take hours or days. That process works well for research; it is slow for a plant that looks wrong right now.',
},
{
title: 'Care integration vs observation logging',
greenlens:
'GreenLens connects identification to care plans, watering reminders, and health checks in one flow.',
competitor:
'iNaturalist focuses on observation logging and scientific accuracy. There is no care guidance, no reminder system, and no disease triage built in.',
},
],
categories: [
{
title: 'Plant identification',
greenlens:
'AI-powered scan results in seconds. Accurate enough for the 450+ common species most plant owners encounter.',
competitor:
'Strong and often highly accurate, especially for unusual or rare species. Community input adds credibility over time.',
whyItMatters:
'If you need ID for a common houseplant right now, both work. For rare or regionally specific species, iNaturalist has a deeper expert pool.',
},
{
title: 'Care guidance after identification',
greenlens:
'Automatic care plan, watering schedule, and contextual next-step recommendations after every scan.',
competitor:
'No care guidance. iNaturalist tells you what the plant is, not what to do with it.',
whyItMatters:
'For plant owners, the ID is only step one. Everything that follows is missing from iNaturalist.',
},
{
title: 'Disease and symptom triage',
greenlens:
'Health check feature analyzes symptoms and narrows down the most likely cause with a clear next action.',
competitor:
'No disease or symptom support. iNaturalist is not designed for diagnostic or rescue workflows.',
whyItMatters:
'If your plant looks sick, iNaturalist gives you a name. GreenLens gives you something to do about it.',
},
{
title: 'Collection and reminders',
greenlens:
'Save plants to your collection, track care history, and receive context-aware watering and fertilizing reminders.',
competitor:
'Observation history exists for logging, but there is no personal collection management or care reminder system.',
whyItMatters:
'Ongoing care requires memory. GreenLens maintains that context; iNaturalist does not.',
},
{
title: 'Citizen science and community',
greenlens:
'No community science layer. GreenLens is a private, individual tool for plant owners.',
competitor:
'This is the core iNaturalist strength. Millions of observations, expert community, and real scientific impact.',
whyItMatters:
'If contributing to biodiversity data or reaching expert naturalists matters to you, iNaturalist is the clear choice.',
},
{
title: 'Offline and rare species depth',
greenlens:
'Identification and health checks require connectivity. Saved collection and care notes are available offline.',
competitor:
'Community data is vast and includes rare, regional, and unusual species that mainstream apps often miss.',
whyItMatters:
'For unusual plants or fieldwork in low-connectivity areas, iNaturalist has meaningful advantages.',
},
],
greenLensBestFor: [
'Plant owners who need care guidance, not just a species name.',
'Anyone dealing with a struggling plant and looking for a concrete next step.',
'People who want reminders, care history, and health checks in one app.',
],
competitorBestFor: [
'Nature enthusiasts who want to document and share biodiversity observations.',
'Users who need rare or unusual species identified by a global expert community.',
'Anyone contributing to citizen science or academic research projects.',
],
emergencyScenarios: [
{
symptom: 'Yellowing leaves on a houseplant',
greenlens:
'GreenLens prompts a health check, asks about recent changes, and surfaces the most likely cause with a recommended next action.',
competitor:
'iNaturalist can confirm the species, but there is no diagnostic flow. You would need to search for care information elsewhere.',
},
{
symptom: 'Unknown plant in the garden — what is it?',
greenlens:
'GreenLens identifies it and immediately adds it to your collection with care guidance.',
competitor:
'iNaturalist is excellent here: fast community confirmation, high accuracy, and a permanent observation record.',
},
{
symptom: 'Soft stems after repotting',
greenlens:
'GreenLens connects the symptom to a likely overwatering or root disturbance scenario and recommends the safest next step.',
competitor:
'Not designed for this use case. iNaturalist has no triage or post-repotting recovery guidance.',
},
],
faqs: [
{
question: 'Is iNaturalist free compared to GreenLens?',
answer:
'iNaturalist is fully free and non-profit. GreenLens includes free functionality with optional paid features for advanced AI scans and unlimited health checks. If cost is the only factor, iNaturalist wins. If you need care guidance alongside identification, GreenLens is the more complete tool.',
},
{
question: 'Can iNaturalist identify plant diseases?',
answer:
'iNaturalist can identify what the plant is, and community members may occasionally comment on visible symptoms. But it has no built-in disease diagnosis, health check workflow, or triage guidance. GreenLens is built specifically for that use case.',
},
{
question: 'Why would someone use GreenLens instead of a free app like iNaturalist?',
answer:
'iNaturalist solves the identification problem well. GreenLens solves what comes after: care scheduling, symptom analysis, collection tracking, and rescue decisions. They are complementary tools for different moments.',
},
{
question: 'Is iNaturalist accurate enough to replace a paid plant app?',
answer:
'For identification alone, iNaturalist is often accurate and sometimes more detailed than paid apps. The gap opens up when you need ongoing care, reminders, or help diagnosing a problem. iNaturalist does not address those needs at all.',
},
],
},
} }
export const competitorOrder: CompetitorSlug[] = ['picturethis', 'plantum'] export const competitorOrder: CompetitorSlug[] = ['picturethis', 'plantum', 'inaturalist']
export function getCompetitorBySlug(slug: string): CompetitorProfile | undefined { export function getCompetitorBySlug(slug: string): CompetitorProfile | undefined {
if (slug === 'picturethis' || slug === 'plantum') { if (slug === 'picturethis' || slug === 'plantum' || slug === 'inaturalist') {
return competitorProfiles[slug] return competitorProfiles[slug]
} }

View File

@@ -63,7 +63,7 @@ export const translations = {
cols: [ cols: [
{ title: 'Produkt', links: ['Features', 'Technologie', 'App laden', 'Support'] }, { title: 'Produkt', links: ['Features', 'Technologie', 'App laden', 'Support'] },
{ title: 'Ressourcen', links: ['So funktioniert es', 'FAQ', 'Support'] }, { title: 'Ressourcen', links: ['So funktioniert es', 'FAQ', 'Support'] },
{ title: 'Rechtliches', links: ['Impressum', 'Datenschutz'] }, { title: 'Rechtliches', links: ['Impressum', 'Datenschutz', 'Nutzungsbedingungen'] },
], ],
}, },
}, },
@@ -129,7 +129,7 @@ export const translations = {
cols: [ cols: [
{ title: 'Product', links: ['Features', 'Technology', 'Get the App', 'Support'] }, { title: 'Product', links: ['Features', 'Technology', 'Get the App', 'Support'] },
{ title: 'Resources', links: ['How it works', 'FAQ', 'Support'] }, { title: 'Resources', links: ['How it works', 'FAQ', 'Support'] },
{ title: 'Legal', links: ['Imprint', 'Privacy Policy'] }, { title: 'Legal', links: ['Imprint', 'Privacy Policy', 'Terms of Service'] },
], ],
}, },
}, },
@@ -195,7 +195,7 @@ export const translations = {
cols: [ cols: [
{ title: 'Producto', links: ['Funciones', 'Tecnologia', 'Descargar', 'Support'] }, { title: 'Producto', links: ['Funciones', 'Tecnologia', 'Descargar', 'Support'] },
{ title: 'Recursos', links: ['Como funciona', 'FAQ', 'Support'] }, { title: 'Recursos', links: ['Como funciona', 'FAQ', 'Support'] },
{ title: 'Legal', links: ['Aviso legal', 'Privacidad'] }, { title: 'Legal', links: ['Aviso legal', 'Privacidad', 'Terminos del servicio'] },
], ],
}, },
}, },

View File

@@ -0,0 +1,439 @@
export interface SeoFeatureRow {
feature: string
greenlens: string
alternative: string
}
export interface SeoFaq {
question: string
answer: string
}
export interface SeoRelatedLink {
href: string
label: string
description: string
}
export interface SeoPageProfile {
slug: string
metaTitle: string
metaDescription: string
canonical: string
h1: string
tagline: string
directAnswer: string
definitionBlock: string
lastUpdated: string
includeAppSchema: boolean
featureTable: {
title: string
alternativeLabel: string
rows: SeoFeatureRow[]
}
greenLensIf: string[]
notBestIf: string[]
faqs: SeoFaq[]
relatedLinks: SeoRelatedLink[]
}
const seoPageProfiles: Record<string, SeoPageProfile> = {
'plant-identifier-app': {
slug: 'plant-identifier-app',
metaTitle: 'Plant Identifier App — GreenLens',
metaDescription:
'GreenLens is a plant identifier app that goes beyond the name. Scan any plant, get the species instantly, and move straight to care guidance, health checks, and rescue decisions.',
canonical: '/plant-identifier-app',
h1: 'Plant Identifier App',
tagline: 'Identify any plant in seconds — then know exactly what to do next.',
directAnswer:
'GreenLens is a plant identifier app for iOS and Android. Point your camera at any plant, tap Scan, and receive the species name, care requirements, and next-step guidance in under a second.',
definitionBlock:
'A plant identifier app uses your phone camera and AI to match a photo against a plant database and return the species name, common names, and care profile. GreenLens extends this with health diagnostics and care scheduling so identification leads directly to action.',
lastUpdated: 'April 2026',
includeAppSchema: true,
featureTable: {
title: 'What separates GreenLens from a basic plant ID app',
alternativeLabel: 'Basic plant ID apps',
rows: [
{
feature: 'Instant plant identification',
greenlens: 'AI scan returns species name, common names, and plant profile in under a second.',
alternative: 'Most apps return a species name and stop there.',
},
{
feature: 'Care guidance after ID',
greenlens: 'Automatic care plan, watering schedule, and light requirements attached to every scan.',
alternative: 'Usually absent or linked to a generic external reference.',
},
{
feature: 'Health check and diagnosis',
greenlens: 'Dedicated health scan analyzes visible symptoms and recommends the safest next action.',
alternative: 'Rarely included. Most ID apps do not address plant emergencies.',
},
{
feature: 'Plant collection',
greenlens: 'Save scanned plants to a personal collection with notes, photos, and care history.',
alternative: 'Scan history only, without ongoing care context.',
},
{
feature: 'Reminders',
greenlens: 'Context-aware care reminders that adapt to your plant and environment.',
alternative: 'Generic calendar reminders not tied to plant condition.',
},
],
},
greenLensIf: [
'You want to know the plant name and immediately understand how to care for it.',
'You are dealing with a plant that looks wrong and need a next step beyond the ID.',
'You want one app for identification, health checks, and ongoing care reminders.',
],
notBestIf: [
'Your main goal is cataloguing rare species for biodiversity research — iNaturalist has a deeper expert community for that.',
'You only need occasional identification and have no interest in ongoing care tracking.',
],
faqs: [
{
question: 'How accurate is GreenLens for plant identification?',
answer:
'GreenLens accurately identifies 450+ plant species including the most common houseplants, garden plants, and succulents. For rare or highly regional species, community platforms such as iNaturalist may have a broader expert pool. For everyday owned plants, GreenLens is fast and reliable.',
},
{
question: 'Does GreenLens work offline?',
answer:
'Scanning and health checks require an internet connection. Your saved plant collection, care notes, and watering reminders are accessible offline.',
},
{
question: 'Is GreenLens free to use as a plant identifier?',
answer:
'GreenLens includes free plant identification. Advanced AI health checks and unlimited scans are available through paid credits or a subscription.',
},
{
question: 'What is the difference between a plant identifier app and a plant care app?',
answer:
'A plant identifier app tells you what the plant is. A plant care app helps you keep it alive. GreenLens is both: it identifies the plant and then provides the care plan, health diagnostics, and reminders you need to act on that information.',
},
{
question: 'Can GreenLens identify plants from photos taken earlier?',
answer:
'Yes. You can upload a photo from your gallery in addition to taking a new scan. The AI analysis works on any clear image of the plant.',
},
],
relatedLinks: [
{
href: '/plant-disease-identifier',
label: 'Plant Disease Identifier',
description: 'Diagnose symptoms and get a concrete next action when your plant looks wrong.',
},
{
href: '/plant-care-app',
label: 'Plant Care App',
description: 'Reminders, care history, and context-aware guidance for every plant you own.',
},
{
href: '/pflanzen-erkennen-app',
label: 'Pflanzen erkennen App',
description: 'Pflanzenerkennung per Foto — direkt mit Pflegeplan und Diagnose.',
},
{
href: '/vs/inaturalist',
label: 'GreenLens vs iNaturalist',
description: 'Compare GreenLens and iNaturalist for plant ID, care, and emergency triage.',
},
],
},
'plant-disease-identifier': {
slug: 'plant-disease-identifier',
metaTitle: 'Plant Disease Identifier — GreenLens',
metaDescription:
'Use GreenLens to identify plant diseases from visible symptoms. Get a concrete next action — not a list of possibilities — when your plant shows yellow leaves, soft stems, or sudden decline.',
canonical: '/plant-disease-identifier',
h1: 'Plant Disease Identifier',
tagline: 'Describe the symptom. Get the most likely cause and a clear next step.',
directAnswer:
'GreenLens identifies plant diseases by analyzing visible symptoms — yellow leaves, brown tips, soft stems, spots, or wilting — and returns the most probable cause with a specific next action, not a list of generic possibilities.',
definitionBlock:
'A plant disease identifier analyzes the visual signs a plant shows — discoloration, texture changes, leaf drop, stem softness — and matches them to known causes such as overwatering, root rot, fungal infection, or nutrient deficiency. GreenLens focuses on the next actionable step rather than an exhaustive diagnosis report.',
lastUpdated: 'April 2026',
includeAppSchema: false,
featureTable: {
title: 'What GreenLens can and cannot do for plant disease',
alternativeLabel: 'Typical disease apps',
rows: [
{
feature: 'Symptom-based analysis',
greenlens: 'Health check scan analyzes the visible symptom pattern and surfaces the most likely cause.',
alternative: 'Often returns a broad list of possible diseases without prioritization.',
},
{
feature: 'Next-step recommendation',
greenlens: 'Recommends one clear action: check the soil, stop fertilizing, isolate the plant, or adjust watering.',
alternative: 'Generic advice such as "improve drainage" or "reduce humidity" without sequencing.',
},
{
feature: 'Context from recent care history',
greenlens: 'Connects symptoms to recent events like repotting, environment changes, or watering frequency.',
alternative: 'Analyzes the photo in isolation without accounting for recent changes.',
},
{
feature: 'Lab-level diagnosis',
greenlens: 'Not designed for professional pathology or agricultural-scale disease tracking.',
alternative: 'Specialized agronomic tools cover industrial and laboratory-grade diagnosis.',
},
{
feature: 'Overcare prevention',
greenlens: 'Specifically designed to stop the most common mistake: adding more care to an already stressed plant.',
alternative: 'Most apps give more tasks, not fewer.',
},
],
},
greenLensIf: [
'Your plant has visible symptoms and you need to know the safest next move.',
'You want to avoid making a stressed plant worse by applying the wrong fix.',
'You have already identified the plant and now need help with the health issue.',
],
notBestIf: [
'You need a laboratory-verified pathology report for commercial or academic use.',
'You are managing a large-scale agricultural operation — specialized agronomic tools are more appropriate.',
],
faqs: [
{
question: 'What plant diseases can GreenLens identify?',
answer:
'GreenLens identifies common disease and stress patterns including overwatering symptoms, root rot signs, underwatering, fungal leaf spots, sunburn, nutrient deficiency indicators, and pest-related damage. It is designed for everyday houseplant and garden scenarios, not rare agricultural pathogens.',
},
{
question: 'Why do my plant leaves keep turning yellow even after I fixed the watering?',
answer:
'Yellow leaves after a care adjustment often indicate root damage from previous overwatering, a nutrient imbalance, or a lighting issue. GreenLens health checks ask about recent care history to narrow down which factor is most likely before recommending the next step.',
},
{
question: 'Can GreenLens tell me if my plant has root rot?',
answer:
'GreenLens can identify the visible signs associated with root rot — soft lower stems, yellowing, wilting despite moist soil, and foul smell — and recommend the appropriate response. It cannot physically inspect the roots, so its analysis is based on the symptom pattern you describe.',
},
{
question: 'Is GreenLens accurate enough to replace a plant professional?',
answer:
'For common household plant diseases and stress patterns, GreenLens is reliable and fast. For rare diseases, serious infestations, or plants with high commercial value, consulting a professional horticulturalist or plant pathologist remains the safer option.',
},
{
question: 'What should I do if GreenLens cannot identify the disease?',
answer:
'Take a sharp, well-lit photo of the affected area, note any recent care changes, and try again. If the result is still uncertain, use the support page to submit details — or consult a local plant specialist for hands-on assessment.',
},
],
relatedLinks: [
{
href: '/plant-identifier-app',
label: 'Plant Identifier App',
description: 'Start with identifying the plant before diagnosing the disease.',
},
{
href: '/plant-care-app',
label: 'Plant Care App',
description: 'Build the care routine that prevents disease in the first place.',
},
],
},
'plant-care-app': {
slug: 'plant-care-app',
metaTitle: 'Plant Care App — GreenLens',
metaDescription:
'GreenLens is a plant care app that goes beyond simple watering reminders. It connects care decisions to what your plant actually needs — not to a generic calendar.',
canonical: '/plant-care-app',
h1: 'Plant Care App',
tagline: 'Care reminders that know your plant — not just your calendar.',
directAnswer:
'GreenLens is a plant care app that combines identification, care scheduling, and health diagnostics in one place. Instead of generic watering timers, it connects care recommendations to the specific plant, its environment, and recent changes.',
definitionBlock:
'A plant care app helps you track watering, fertilizing, and maintenance schedules for each plant you own. GreenLens extends this with AI-based care plans derived from the scan result, context-aware reminders, and health check capability so care decisions stay grounded in what the plant actually shows.',
lastUpdated: 'April 2026',
includeAppSchema: true,
featureTable: {
title: 'GreenLens vs a basic reminder app',
alternativeLabel: 'Basic reminder apps',
rows: [
{
feature: 'Care scheduling',
greenlens: 'Reminders derived from the identified plant profile, adjusted to your home environment.',
alternative: 'Manual timers set by the user without plant-specific context.',
},
{
feature: 'Watering guidance',
greenlens: 'Considers soil feel, recent weather, and season — not just elapsed days.',
alternative: 'Fixed interval (e.g. every 7 days) regardless of plant condition.',
},
{
feature: 'Health connection',
greenlens: 'Care history links directly to health check results so you can see if routine care is causing problems.',
alternative: 'Reminders and diagnosis are separate with no shared context.',
},
{
feature: 'Plant collection',
greenlens: 'Per-plant care profiles with notes, photo history, and individual reminder schedules.',
alternative: 'Single shared reminder list with no per-plant differentiation.',
},
{
feature: 'Care plan on first scan',
greenlens: 'Identification automatically generates a starter care plan — no manual setup required.',
alternative: 'User must research and configure every parameter manually.',
},
],
},
greenLensIf: [
'You want care reminders that are based on the actual plant, not a generic schedule.',
'You have multiple plants and need per-plant care profiles in one place.',
'You want to connect care history to health diagnosis when something goes wrong.',
],
notBestIf: [
'You only need a simple universal timer without any plant-specific context.',
'You are managing a professional nursery or large-scale growing operation — commercial tools are more appropriate.',
],
faqs: [
{
question: 'How is GreenLens different from a basic watering reminder app?',
answer:
'A basic watering reminder fires every N days regardless of what the plant looks like. GreenLens connects care recommendations to the specific species, your environment, and what changed recently. If a plant is already stressed, the reminder approach adjusts rather than pushing a routine that makes things worse.',
},
{
question: 'Can GreenLens remind me to fertilize and repot as well as water?',
answer:
'Yes. GreenLens care plans include watering, fertilizing, and repotting schedules tailored to the identified species. Each reminder type can be adjusted individually per plant.',
},
{
question: 'How many plants can I track in GreenLens?',
answer:
'GreenLens supports a personal collection of multiple plants. Free and paid tiers differ on the number of advanced AI health checks available, but collection management and basic care reminders are included in the free version.',
},
{
question: 'Will GreenLens tell me if I am overwatering?',
answer:
'Yes. The health check feature is specifically designed to catch overwatering before it becomes root rot. If you scan a plant showing soft stems or yellowing and the care history shows recent watering, GreenLens will flag the likely connection.',
},
{
question: 'Does GreenLens work for outdoor plants as well as houseplants?',
answer:
'GreenLens covers both indoor and outdoor plants. Care plan recommendations account for the plant type, so outdoor and garden plants receive contextually different guidance than tropical houseplants.',
},
],
relatedLinks: [
{
href: '/plant-identifier-app',
label: 'Plant Identifier App',
description: 'Identify the plant first — then the care plan generates automatically.',
},
{
href: '/plant-disease-identifier',
label: 'Plant Disease Identifier',
description: 'When the care routine is not enough and the plant starts showing symptoms.',
},
],
},
'pflanzen-erkennen-app': {
slug: 'pflanzen-erkennen-app',
metaTitle: 'Pflanzen erkennen App — GreenLens',
metaDescription:
'GreenLens erkennt Pflanzen per Foto in Sekunden und liefert sofort einen Pflegeplan, Gießerinnerungen und Gesundheitsdiagnosen — alles in einer App.',
canonical: '/pflanzen-erkennen-app',
h1: 'Pflanzen erkennen App',
tagline: 'Pflanze fotografieren — Name, Pflegeanleitung und Diagnose in einer Sekunde.',
directAnswer:
'GreenLens ist eine Pflanzenerkennungs-App für iOS und Android. Einfach die Kamera auf eine Pflanze richten, scannen — und sofort erscheinen Artname, Pflegebedarf und nächste Handlungsempfehlung.',
definitionBlock:
'Eine Pflanzen-App erkennt Pflanzen anhand von Fotos und liefert den Artnamen sowie Pflegeinformationen. GreenLens geht weiter: Jeder Scan erzeugt automatisch einen Pflegeplan, und ein separater Gesundheitscheck analysiert Symptome wie gelbe Blätter oder weiche Stiele.',
lastUpdated: 'April 2026',
includeAppSchema: true,
featureTable: {
title: 'GreenLens im Vergleich zu einfachen Erkennungs-Apps',
alternativeLabel: 'Einfache Erkennungs-Apps',
rows: [
{
feature: 'Pflanzenerkennung per Foto',
greenlens: 'KI-gestützter Scan liefert Artname, Trivialname und Pflanzenportrait in unter einer Sekunde.',
alternative: 'Artname wird ausgegeben — ohne weitere Informationen oder nächste Schritte.',
},
{
feature: 'Automatischer Pflegeplan',
greenlens: 'Gießen, Düngen und Umtopfen werden nach dem Scan direkt als individueller Plan erstellt.',
alternative: 'Pflege muss manuell recherchiert und eingetragen werden.',
},
{
feature: 'Gesundheitscheck',
greenlens: 'Eigener Scan für Symptome wie gelbe Blätter, weiche Stiele oder plötzlichen Rückgang — mit klarer Handlungsempfehlung.',
alternative: 'Kaum vorhanden. Die meisten Erkennungs-Apps bieten keine Diagnose.',
},
{
feature: 'Pflanzensammlung',
greenlens: 'Eigene Sammlung mit Pflegeverläufen, Fotos und Erinnerungen pro Pflanze.',
alternative: 'Nur Scan-Verlauf, kein dauerhafter Pflegekontext.',
},
{
feature: 'Mehrsprachigkeit',
greenlens: 'Vollständig auf Deutsch, Englisch und Spanisch verfügbar.',
alternative: 'Häufig nur Englisch oder mit unvollständiger Übersetzung.',
},
],
},
greenLensIf: [
'Du willst eine Pflanze sofort bestimmen und direkt wissen, wie du sie pflegst.',
'Du hast eine Pflanze, die krank aussieht, und brauchst einen konkreten nächsten Schritt.',
'Du möchtest Pflanzen sammeln, Pflegeerinnerungen setzen und Gesundheitsprobleme diagnostizieren — in einer App.',
],
notBestIf: [
'Du möchtest seltene Wildpflanzen für die Citizen Science dokumentieren — dafür ist iNaturalist besser geeignet.',
'Du benötigst nur gelegentliche Bestimmung ohne Pflege- oder Diagnosefunktionen.',
],
faqs: [
{
question: 'Wie genau erkennt GreenLens Pflanzen?',
answer:
'GreenLens erkennt über 450 Pflanzenarten zuverlässig — darunter die häufigsten Zimmerpflanzen, Gartenpflanzen und Sukkulenten. Bei seltenen oder regional spezifischen Arten kann die Community-Plattform iNaturalist mehr Expertenwissen bieten. Für Alltagspflanzen ist GreenLens schnell und treffsicher.',
},
{
question: 'Funktioniert GreenLens auch ohne Internetverbindung?',
answer:
'Scans und Gesundheitschecks benötigen eine Internetverbindung. Die gespeicherte Pflanzensammlung, Pflegenotizen und Gießerinnerungen sind jedoch offline verfügbar.',
},
{
question: 'Ist GreenLens kostenlos nutzbar?',
answer:
'GreenLens enthält kostenlose Pflanzenerkennung. Erweiterte KI-Gesundheitschecks und unbegrenzte Scans sind über kostenpflichtige Credits oder ein Abonnement verfügbar.',
},
{
question: 'Kann GreenLens auch Pflanzenkrankheiten erkennen?',
answer:
'Ja. Der Gesundheitscheck analysiert sichtbare Symptome wie gelbe Blätter, braune Spitzen, weiche Stiele oder Flecken und liefert die wahrscheinlichste Ursache sowie einen konkreten nächsten Schritt — zum Beispiel Gießen einstellen, Standort prüfen oder isolieren.',
},
{
question: 'Welche Sprachen unterstützt GreenLens?',
answer:
'GreenLens ist vollständig auf Deutsch, Englisch und Spanisch verfügbar. Die Sprache kann in der App jederzeit gewechselt werden.',
},
],
relatedLinks: [
{
href: '/plant-identifier-app',
label: 'Plant Identifier App (English)',
description: 'The English version of this page for plant identification and care.',
},
{
href: '/plant-disease-identifier',
label: 'Plant Disease Identifier',
description: 'Symptom-based diagnosis when your plant starts showing problems.',
},
{
href: '/plant-care-app',
label: 'Plant Care App',
description: 'Reminders and care tracking for every plant in your collection.',
},
],
},
}
export function getSeoPageBySlug(slug: string): SeoPageProfile | undefined {
return seoPageProfiles[slug]
}

33
greenlns-landing/proxy.ts Normal file
View File

@@ -0,0 +1,33 @@
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
const APEX_HOST = 'greenlenspro.com'
const WWW_HOST = `www.${APEX_HOST}`
export function proxy(request: NextRequest) {
const host = request.headers.get('host')
const forwardedProto = request.headers.get('x-forwarded-proto')
if (!host || (host !== APEX_HOST && host !== WWW_HOST)) {
return NextResponse.next()
}
const url = request.nextUrl.clone()
let shouldRedirect = false
if (host === WWW_HOST) {
url.host = APEX_HOST
shouldRedirect = true
}
if (forwardedProto === 'http' || url.protocol === 'http:') {
url.protocol = 'https:'
shouldRedirect = true
}
return shouldRedirect ? NextResponse.redirect(url, 308) : NextResponse.next()
}
export const config = {
matcher: '/:path*',
}