SEO blogpost

This commit is contained in:
2026-08-05 19:39:22 +02:00
parent d7432fa65c
commit cf05e60251
53 changed files with 10592 additions and 125 deletions

View File

@@ -0,0 +1,59 @@
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { blogPosts } from '@/lib/blogPosts'
import { BlogPostTemplate } from '@/components/blog/BlogPostTemplate'
import { getHreflangAlternates } from '@/lib/localeRoutes'
interface Props {
params: Promise<{ slug: string }>
}
export async function generateStaticParams() {
return Object.keys(blogPosts).map((slug) => ({ slug }))
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
const post = blogPosts[slug]
if (!post) return {}
const languages = getHreflangAlternates(post.canonical)
return {
title: post.metaTitle,
description: post.metaDescription,
// Selbstreferenzierender Canonical. Wichtig für die Syndication: Wenn der
// Beitrag auf Medium, DEV.to oder LinkedIn zweitveröffentlicht wird, muss
// dort ein rel=canonical auf genau diese URL zeigen.
alternates: {
canonical: post.canonical,
...(languages && { languages }),
},
openGraph: {
type: 'article',
title: post.metaTitle,
description: post.metaDescription,
url: post.canonical,
publishedTime: post.publishedAtIso,
modifiedTime: post.updatedAtIso || post.publishedAtIso,
authors: [post.author.name],
images: [{ url: post.heroImage, width: 1200, height: 630, alt: post.heroImageAlt }],
},
twitter: {
card: 'summary_large_image',
title: post.metaTitle,
description: post.metaDescription,
images: [post.heroImage],
},
}
}
export default async function BlogPostPage({ params }: Props) {
const { slug } = await params
const post = blogPosts[slug]
if (!post) {
notFound()
}
return <BlogPostTemplate post={post} />
}

View File

@@ -0,0 +1,22 @@
import { Metadata } from 'next'
import { blogPosts } from '@/lib/blogPosts'
import { BlogPostTemplate } from '@/components/blog/BlogPostTemplate'
const post = blogPosts['houseplant-watering-schedule']
export const metadata: Metadata = {
title: post.metaTitle,
description: post.metaDescription,
alternates: {
canonical: post.canonical,
},
openGraph: {
title: post.metaTitle,
description: post.metaDescription,
images: [{ url: post.heroImage }],
},
}
export default function HouseplantWateringSchedulePage() {
return <BlogPostTemplate post={post} />
}

View File

@@ -0,0 +1,97 @@
import React from 'react'
import { Metadata } from 'next'
import Link from 'next/link'
import SeoNavbar from '@/components/seo/SeoNavbar'
import SeoFooter from '@/components/seo/SeoFooter'
import { blogPosts } from '@/lib/blogPosts'
export const metadata: Metadata = {
title: 'GreenLens Plant Care & Identification Blog',
description:
'Expert guides on houseplant watering schedules, plant disease diagnosis, pest control, and photo identification.',
alternates: {
canonical: '/blog',
},
}
export default function BlogIndexPage() {
const posts = Object.values(blogPosts)
return (
<div className="bg-[#fdfbf6] text-on-surface font-body antialiased min-h-screen relative overflow-hidden flex flex-col justify-between">
{/* Header Navigation */}
<SeoNavbar locale="en" />
{/* Decorative Ambient Green Blobs */}
<div className="absolute inset-0 pointer-events-none -z-10" aria-hidden="true">
<div className="absolute top-[-5%] left-[-10%] w-[60vw] h-[60vw] max-w-[800px] max-h-[800px] bg-[#c2e5cc] blur-[120px] rounded-full mix-blend-multiply opacity-70" />
<div className="absolute top-[25%] right-[-5%] w-[50vw] h-[50vw] max-w-[600px] max-h-[600px] bg-[#e5f0cb] blur-[100px] rounded-full mix-blend-multiply opacity-80" />
</div>
{/* Main Content */}
<main className="pt-32 pb-24 flex-1">
<div className="max-w-[1100px] mx-auto px-6">
{/* Hero Header */}
<div className="text-center max-w-[750px] mx-auto mb-16">
<span className="bg-emerald-600/10 text-emerald-800 border border-emerald-600/20 text-xs font-extrabold tracking-widest uppercase px-4 py-1.5 rounded-full inline-block mb-4 shadow-xs">
🌿 Botanical Research & Guides
</span>
<h1 className="font-display text-4xl sm:text-5xl font-bold text-emerald-950 mb-4 tracking-tight leading-tight">
Plant Care & Identification Guides
</h1>
<p className="text-emerald-900/80 text-lg sm:text-xl font-medium leading-relaxed">
Evidence-based guides on indoor plant watering schedules, health diagnostics, pest control, and species identification.
</p>
</div>
{/* Blog Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{posts.map((post) => (
<Link
key={post.slug}
href={`/blog/${post.slug}`}
className="group bg-white rounded-3xl border-2 border-emerald-600/20 overflow-hidden shadow-sm hover:shadow-2xl hover:border-emerald-600/40 transition-all duration-300 flex flex-col"
>
{/* Image */}
<div className="relative h-52 w-full overflow-hidden bg-emerald-950">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={post.heroImage}
alt={post.heroImageAlt}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/>
<div className="absolute top-4 left-4 bg-emerald-950/90 backdrop-blur-md px-3.5 py-1 rounded-full text-[10px] font-extrabold uppercase tracking-wider text-emerald-300 border border-emerald-500/30">
{post.category}
</div>
</div>
{/* Card Content */}
<div className="p-6 flex-1 flex flex-col justify-between">
<div>
<div className="text-xs text-emerald-800 font-semibold mb-2">
{post.publishedAt} {post.readTime}
</div>
<h2 className="font-display text-xl font-bold text-emerald-950 group-hover:text-emerald-700 transition-colors mb-3 leading-snug">
{post.title}
</h2>
<p className="text-emerald-900/70 text-sm line-clamp-3 leading-relaxed mb-6 font-normal">
{post.subtitle}
</p>
</div>
<div className="text-emerald-700 font-bold text-sm flex items-center gap-1.5 group-hover:translate-x-1 transition-transform">
<span>Read Full Guide</span>
<span></span>
</div>
</div>
</Link>
))}
</div>
</div>
</main>
{/* Footer Navigation */}
<SeoFooter locale="en" />
</div>
)
}

View File

@@ -0,0 +1,22 @@
import { Metadata } from 'next'
import { blogPosts } from '@/lib/blogPosts'
import { BlogPostTemplate } from '@/components/blog/BlogPostTemplate'
const post = blogPosts['plant-health-diagnosis-guide']
export const metadata: Metadata = {
title: post.metaTitle,
description: post.metaDescription,
alternates: {
canonical: post.canonical,
},
openGraph: {
title: post.metaTitle,
description: post.metaDescription,
images: [{ url: post.heroImage }],
},
}
export default function PlantHealthDiagnosisGuidePage() {
return <BlogPostTemplate post={post} />
}

View File

@@ -0,0 +1,22 @@
import { Metadata } from 'next'
import { blogPosts } from '@/lib/blogPosts'
import { BlogPostTemplate } from '@/components/blog/BlogPostTemplate'
const post = blogPosts['which-plant-is-this-guide']
export const metadata: Metadata = {
title: post.metaTitle,
description: post.metaDescription,
alternates: {
canonical: post.canonical,
},
openGraph: {
title: post.metaTitle,
description: post.metaDescription,
images: [{ url: post.heroImage }],
},
}
export default function WhichPlantIsThisGuidePage() {
return <BlogPostTemplate post={post} />
}

View File

@@ -23,6 +23,18 @@ export const metadata: Metadata = {
},
}
const engineeringPosts = [
{ slug: 'vision-ai-plant-disease-diagnostic', title: 'Building a Plant Disease Diagnostic Engine with Vision AI & Multi-Symptom Scoring', readTime: '8 min read' },
{ slug: 'chrome-extension-manifest-v3-image-scanner', title: 'How to Build a Lightweight Chrome Extension (Manifest V3) for Real-Time Image & Canvas Inspection', readTime: '7 min read' },
{ slug: 'zero-dependency-python-sdk', title: 'Designing a Zero-Dependency Python SDK for REST APIs with Automatic Retries & Rate-Limit Backoff', readTime: '7 min read' },
{ slug: 'react-native-offline-first-sync', title: 'Offline-First Mobile Architecture: Syncing Local SQLite Storage with Remote AI Services in React Native', readTime: '8 min read' },
{ slug: 'typescript-cli-automation-tool', title: 'Building a CLI Tool in TypeScript for Automated Image & Asset Diagnostic Workflows', readTime: '6 min read' },
{ slug: 'computer-vision-edge-cases-plant-health', title: 'Computer Vision Edge Cases: Why Plant Models Agree on Species But Disagree on Health', readTime: '7 min read' },
{ slug: 'programmatic-seo-nextjs-app-router', title: 'Programmatic SEO Infrastructure with Next.js App Router: Type-Safe Page Factories', readTime: '7 min read' },
{ slug: 'automated-content-syndication-engine', title: 'Automated Content Syndication: Publishing Developer Posts to DEV.to and Hashnode', readTime: '6 min read' },
{ slug: 'cross-platform-design-system-tokens', title: 'Building a Cross-Platform Design System for Web (Next.js) and Mobile (React Native)', readTime: '7 min read' },
]
const tools = [
{
icon: '💻',
@@ -54,11 +66,11 @@ const tools = [
{
icon: '🧩',
eyebrow: 'Chrome Web Store',
title: 'Chrome extension (Soon)',
title: 'Chrome extension',
description: 'Search a local plant catalog, check leaf symptoms, or select a plant photo from the current browser page for AI analysis.',
command: 'Browser popup + photo scan tool',
href: 'https://greenlenspro.com/support',
linkLabel: 'Chrome extension details (Soon)',
href: siteConfig.chromeWebStoreUrl,
linkLabel: 'Chrome Web Store',
},
{
icon: '🐙',
@@ -149,6 +161,34 @@ export default function DevelopersPage() {
</div>
</section>
<section className="py-20 md:py-28 border-b border-outline-variant/60" id="engineering-blog">
<div className="max-w-[1200px] mx-auto px-6">
<div className="max-w-2xl mb-12 md:mb-16">
<p className="text-xs font-bold tracking-[0.18em] uppercase text-primary mb-5">Engineering blog</p>
<h2 className="font-display text-4xl md:text-5xl font-bold leading-tight text-ink">How we built the GreenLens platform.</h2>
<p className="mt-5 max-w-2xl text-base leading-relaxed text-on-surface-variant">
Deep dives into the real architecture behind GreenLens: the vision AI diagnostic pipeline, the Chrome extension, the Python SDK, offline-first sync, and more.
</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
{engineeringPosts.map((post) => (
<Link
key={post.slug}
href={`/blog/${post.slug}`}
className="flex flex-col rounded-2xl bg-white p-6 border border-outline-variant/70 transition-transform duration-300 hover:-translate-y-1 hover:shadow-[0_18px_40px_-28px_rgba(1,38,31,0.38)]"
>
<span className="text-[10px] font-bold tracking-[0.15em] uppercase text-primary">{post.readTime}</span>
<h3 className="mt-3 font-display text-lg font-bold leading-snug text-ink">{post.title}</h3>
<span className="mt-4 inline-flex w-fit items-center text-sm font-bold text-primary">
Read the post <span aria-hidden="true" className="ml-2"></span>
</span>
</Link>
))}
</div>
</div>
</section>
<section className="py-20 md:py-28" id="faq">
<div className="max-w-[1200px] mx-auto px-6 grid gap-8 lg:grid-cols-[1fr_auto] lg:items-end">
<div>

View File

@@ -0,0 +1,4 @@
import { buildSeoPageMetadata, createSeoPage } from '@/lib/seoPageFactory'
export const metadata = buildSeoPageMetadata('giessplan-zimmerpflanzen')
export default createSeoPage('giessplan-zimmerpflanzen')

View File

@@ -0,0 +1,4 @@
import { buildSeoPageMetadata, createSeoPage } from '@/lib/seoPageFactory'
export const metadata = buildSeoPageMetadata('pflanzen-diagnose')
export default createSeoPage('pflanzen-diagnose')

View File

@@ -1,6 +1,7 @@
import { MetadataRoute } from 'next'
import { germanSeoPageSlugs, getGermanSeoPageBySlug } from '@/lib/seoPages'
import { spanishSeoPageProfiles, spanishSeoPageSlugs } from '@/lib/spanishSeoPages'
import { blogPosts } from '@/lib/blogPosts'
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = (process.env.NEXT_PUBLIC_SITE_URL || 'https://greenlenspro.com').trim()
@@ -153,6 +154,18 @@ export default function sitemap(): MetadataRoute.Sitemap {
changeFrequency: 'monthly',
priority: 0.85,
},
{
url: `${baseUrl}/blog`,
lastModified: new Date('2026-08-04'),
changeFrequency: 'weekly',
priority: 0.85,
},
...Object.keys(blogPosts).map((slug) => ({
url: `${baseUrl}/blog/${slug}`,
lastModified: new Date('2026-08-05'),
changeFrequency: 'monthly' as const,
priority: 0.8,
})),
...spanishSeoPageSlugs.map((slug) => ({
url: `${baseUrl}${spanishSeoPageProfiles[slug].canonical}`,
lastModified: new Date('2026-07-10'),

View File

@@ -0,0 +1,4 @@
import { buildSeoPageMetadata, createSeoPage } from '@/lib/seoPageFactory'
export const metadata = buildSeoPageMetadata('welche-pflanze-ist-das')
export default createSeoPage('welche-pflanze-ist-das')

View File

@@ -0,0 +1,4 @@
import { buildSeoPageMetadata, createSeoPage } from '@/lib/seoPageFactory'
export const metadata = buildSeoPageMetadata('wie-funktioniert-pflanzenerkennung')
export default createSeoPage('wie-funktioniert-pflanzenerkennung')

View File

@@ -0,0 +1,4 @@
import { buildSeoPageMetadata, createSeoPage } from '@/lib/seoPageFactory'
export const metadata = buildSeoPageMetadata('zimmerpflanzen-mit-wenig-licht')
export default createSeoPage('zimmerpflanzen-mit-wenig-licht')

View File

@@ -89,7 +89,7 @@ export default function Footer() {
<a href="https://marketplace.visualstudio.com/items?itemName=TimoKnuth.greenlens-vscode" target="_blank" rel="noopener noreferrer">VS Code Extension</a>
<a href="https://pypi.org/project/greenlens-python/" target="_blank" rel="noopener noreferrer">Python SDK &amp; CLI (PyPI)</a>
<a href="https://www.npmjs.com/package/greenlenspro-cli" target="_blank" rel="noopener noreferrer">Node.js CLI (NPM)</a>
<Link href="/developers">Chrome Extension (Soon)</Link>
<a href={siteConfig.chromeWebStoreUrl} target="_blank" rel="noopener noreferrer">Chrome Extension</a>
<a href="https://github.com/knuthtimo-lab/greenlenspro-cli" target="_blank" rel="noopener noreferrer">GitHub Action &amp; Repository</a>
</div>
</div>

View File

@@ -0,0 +1,525 @@
import React from 'react'
import Link from 'next/link'
import SeoNavbar from '@/components/seo/SeoNavbar'
import SeoFooter from '@/components/seo/SeoFooter'
import { BlogPost } from '@/lib/blogPosts'
const SITE = 'https://greenlenspro.com'
/**
* JSON-LD für Blogposts. BlogPosting liefert Autor und Datum, FAQPage und HowTo
* sind die beiden Formate, aus denen AI-Antwortsysteme direkt extrahieren.
* Ohne diese Blöcke bleibt der Beitrag für sie unstrukturierter Fließtext.
*/
function buildBlogSchema(post: BlogPost) {
const graph: Record<string, unknown>[] = [
{
'@type': 'BlogPosting',
'@id': `${SITE}${post.canonical}#article`,
headline: post.title,
description: post.metaDescription,
image: `${SITE}${post.heroImage}`,
datePublished: post.publishedAtIso,
dateModified: post.updatedAtIso || post.publishedAtIso,
inLanguage: post.locale,
mainEntityOfPage: { '@type': 'WebPage', '@id': `${SITE}${post.canonical}` },
author: {
'@type': 'Person',
name: post.author.name,
jobTitle: post.author.role,
},
publisher: {
'@type': 'Organization',
name: 'GreenLens',
url: SITE,
},
...(post.sources?.length
? { citation: post.sources.map((source) => ({ '@type': 'CreativeWork', name: source.title, url: source.url })) }
: {}),
},
]
if (post.faqs?.length) {
graph.push({
'@type': 'FAQPage',
'@id': `${SITE}${post.canonical}#faq`,
mainEntity: post.faqs.map((faq) => ({
'@type': 'Question',
name: faq.question,
acceptedAnswer: { '@type': 'Answer', text: faq.answer },
})),
})
}
if (post.howToSteps?.length) {
graph.push({
'@type': 'HowTo',
'@id': `${SITE}${post.canonical}#howto`,
name: post.howToName || post.title,
step: post.howToSteps.map((step, index) => ({
'@type': 'HowToStep',
position: index + 1,
name: step.name.replace(/^\d+\.\s*/, ''),
text: step.text,
})),
})
}
return { '@context': 'https://schema.org', '@graph': graph }
}
function renderFormattedText(text: string) {
if (!text) return null
const regex = /\[([^\]]+)\]\(([^)]+)\)/g
const parts: React.ReactNode[] = []
let lastIndex = 0
let match: RegExpExecArray | null
while ((match = regex.exec(text)) !== null) {
if (match.index > lastIndex) {
parts.push(text.substring(lastIndex, match.index))
}
const linkText = match[1]
const url = match[2]
const isExternal = url.startsWith('http')
if (isExternal) {
parts.push(
<a
key={match.index}
href={url}
target="_blank"
rel="noopener noreferrer"
className="text-emerald-700 underline underline-offset-2 hover:text-emerald-900 font-semibold"
>
{linkText}
</a>
)
} else {
parts.push(
<Link
key={match.index}
href={url}
className="text-emerald-700 underline underline-offset-2 hover:text-emerald-900 font-semibold"
>
{linkText}
</Link>
)
}
lastIndex = regex.lastIndex
}
if (lastIndex < text.length) {
parts.push(text.substring(lastIndex))
}
return parts.length > 0 ? parts : text
}
export function BlogPostTemplate({ post }: { post: BlogPost }) {
return (
<div className="bg-[#fdfbf6] text-on-surface font-body antialiased min-h-screen relative overflow-hidden">
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(buildBlogSchema(post)) }}
/>
{/* Navigation Header */}
<SeoNavbar locale="en" />
{/* Decorative Green Glow Blobs */}
<div className="absolute inset-0 pointer-events-none -z-10" aria-hidden="true">
<div className="absolute top-[-5%] left-[-10%] w-[60vw] h-[60vw] max-w-[800px] max-h-[800px] bg-[#c2e5cc] blur-[120px] rounded-full mix-blend-multiply opacity-70" />
<div className="absolute top-[20%] right-[-5%] w-[50vw] h-[50vw] max-w-[600px] max-h-[600px] bg-[#e5f0cb] blur-[100px] rounded-full mix-blend-multiply opacity-80" />
<div className="absolute top-[50%] left-[10%] w-[40vw] h-[40vw] max-w-[500px] max-h-[500px] bg-[#a8e0b8] blur-[110px] rounded-full mix-blend-multiply opacity-60" />
</div>
{/* Main Container */}
<main className="pt-28 pb-20">
{/* Header / Breadcrumb & Meta */}
<header className="py-12 bg-white/80 backdrop-blur-md border-b border-emerald-900/10 shadow-xs">
<div className="max-w-[1320px] mx-auto px-6">
{/* Breadcrumbs */}
<nav className="flex items-center gap-2 text-xs text-emerald-800/70 mb-6 font-semibold tracking-wide">
<Link href="/" className="hover:text-emerald-700 transition-colors">
Home
</Link>
<span>/</span>
<Link href="/blog" className="hover:text-emerald-700 transition-colors">
Blog
</Link>
<span>/</span>
<span className="text-emerald-700 font-bold">{post.category}</span>
</nav>
{/* Category & Read Time */}
<div className="flex items-center gap-3 mb-5">
<span className="bg-emerald-600/10 text-emerald-800 border border-emerald-600/20 text-xs font-extrabold tracking-wider uppercase px-3.5 py-1 rounded-full shadow-xs">
🌿 {post.category}
</span>
<span className="text-xs text-emerald-900/70 font-semibold">
{post.readTime}
</span>
</div>
{/* Title & Subtitle */}
<h1 className="font-display text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight text-emerald-950 mb-5 leading-[1.15]">
{post.title}
</h1>
<p className="text-lg md:text-xl text-emerald-900/80 leading-relaxed max-w-[850px] mb-8 font-medium">
{post.subtitle}
</p>
{/* Author Info */}
<div className="flex items-center gap-4 pt-6 border-t border-emerald-900/10">
<div className="w-12 h-12 rounded-full bg-emerald-600 text-white font-bold text-lg flex items-center justify-center shadow-md border-2 border-emerald-400">
🌱
</div>
<div>
<div className="font-bold text-sm text-emerald-950">{post.author.name}</div>
<div className="text-xs text-emerald-700 font-medium">
{post.author.role} Updated {post.publishedAt}
</div>
</div>
</div>
</div>
</header>
{/* Content & Sidebar Grid */}
<div className="max-w-[1320px] mx-auto px-6 pt-10 grid grid-cols-1 lg:grid-cols-12 gap-12">
{/* Main Article Content */}
<div className="lg:col-span-8">
{/* Featured Hero Image */}
<div className="relative w-full h-[360px] sm:h-[450px] rounded-2xl overflow-hidden shadow-2xl mb-10 border-2 border-emerald-900/10 group">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={post.heroImage}
alt={post.heroImageAlt}
className="w-full h-full object-cover"
/>
{/* Dynamic Hero Overlay Badge */}
<div className="absolute bottom-6 left-6 right-6 sm:right-auto max-w-[350px] bg-white/95 backdrop-blur-md p-4 rounded-xl border-2 border-emerald-600/30 flex items-center gap-3 shadow-2xl">
<div className="w-2 h-10 bg-emerald-600 rounded-full shrink-0"></div>
<div>
<div className="font-display font-bold text-sm text-emerald-950 mb-0.5">
{post.heroImageBadgeTitle}
</div>
<div className="text-[10px] font-extrabold tracking-[0.15em] uppercase text-emerald-700">
{post.heroImageBadgeLabel}
</div>
</div>
</div>
</div>
{/* Direct Answer Summary Callout Box (AI-Overview Ready) */}
{post.directAnswer && (
<div className="bg-emerald-500/10 border-l-4 border-emerald-600 p-6 rounded-r-2xl mb-10 shadow-sm border-y border-r border-emerald-600/20">
<div className="flex items-center gap-2 text-emerald-800 font-extrabold text-xs uppercase tracking-wider mb-2">
<span> Quick Botanical Answer</span>
</div>
<p className="text-emerald-950 font-medium leading-relaxed text-base max-w-[72ch]">
{renderFormattedText(post.directAnswer)}
</p>
</div>
)}
{/* Mobile TOC */}
{post.toc && post.toc.length > 0 && (
<div className="lg:hidden bg-white p-6 rounded-2xl border-2 border-emerald-600/20 mb-10 shadow-sm">
<div className="font-extrabold text-xs text-emerald-800 uppercase tracking-widest mb-3">
Table of Contents
</div>
<ul className="space-y-2.5 text-sm font-medium">
{post.toc.map((item) => (
<li key={item.id}>
<a href={`#${item.id}`} className="text-emerald-700 hover:text-emerald-900 hover:underline">
{item.label}
</a>
</li>
))}
</ul>
</div>
)}
{/* Body Sections */}
<div className="space-y-12 text-emerald-950 text-base leading-relaxed">
{post.sections.map((section, sIdx) => (
<React.Fragment key={section.id}>
<section id={section.id} className="scroll-mt-28">
{section.eyebrow && (
<div className="text-xs font-extrabold tracking-widest text-emerald-700 uppercase mb-1">
{section.eyebrow}
</div>
)}
<h2 className="font-display text-2xl sm:text-3xl font-bold text-emerald-950 mb-4">
{section.title}
</h2>
<p className="text-emerald-900/80 text-base sm:text-lg leading-relaxed mb-6 font-normal max-w-[68ch]">
{renderFormattedText(section.body)}
</p>
{/* Bullets */}
{section.bullets && section.bullets.length > 0 && (
<ul className="space-y-3 mb-6 bg-white p-6 rounded-2xl border border-emerald-600/20 shadow-xs">
{section.bullets.map((bullet, idx) => (
<li key={idx} className="flex items-start gap-3 text-sm sm:text-base text-emerald-900">
<span className="w-2.5 h-2.5 rounded-full bg-emerald-600 mt-2 shrink-0 shadow-xs" />
<span className="max-w-[76ch]">{renderFormattedText(bullet)}</span>
</li>
))}
</ul>
)}
{/* Table */}
{section.table && (
<div className="overflow-x-auto mb-8 rounded-2xl border-2 border-emerald-600/20 shadow-sm bg-white">
<table className="w-full text-left border-collapse text-sm">
<thead className="bg-emerald-900 text-white font-bold">
<tr>
{section.table.headers.map((h, i) => (
<th key={i} className="p-3.5 sm:p-4 border-b border-emerald-800">
{h}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-emerald-600/10">
{section.table.rows.map((row, rIdx) => (
<tr key={rIdx} className="hover:bg-emerald-500/5 transition-colors">
{row.map((cell, cIdx) => (
<td
key={cIdx}
className={`p-3.5 sm:p-4 text-emerald-900 ${
cIdx === 0 ? 'font-bold text-emerald-950' : ''
}`}
>
{renderFormattedText(cell)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Callout */}
{section.callout && (
<div
className={`p-5 rounded-2xl border-2 mb-6 ${
section.callout.type === 'warning'
? 'bg-amber-500/10 border-amber-500/30 text-amber-950'
: 'bg-emerald-500/10 border-emerald-600/30 text-emerald-950'
}`}
>
<div className="font-bold text-sm mb-1">{section.callout.title}</div>
<div className="text-sm opacity-90">{renderFormattedText(section.callout.body)}</div>
</div>
)}
</section>
{/* Mid-Content Conversion CTA Widget after 2nd section */}
{sIdx === 1 && (
<div className="my-10 p-6 sm:p-8 rounded-2xl bg-gradient-to-br from-emerald-900 via-emerald-950 to-ink text-white border-2 border-emerald-400/30 shadow-xl flex flex-col sm:flex-row items-center justify-between gap-6">
<div className="space-y-1.5 text-center sm:text-left">
<div className="flex items-center justify-center sm:justify-start gap-2 text-xs font-extrabold tracking-widest text-emerald-400 uppercase">
<span>🌿 Instant AI Health Diagnosis</span>
</div>
<h3 className="font-display text-xl sm:text-2xl font-bold text-white">
Unsure what is wrong with your plant?
</h3>
<p className="text-white/80 text-sm max-w-[480px]">
Scan your plant leaf with GreenLens AI for an immediate disease check, pest identification, and custom care plan.
</p>
</div>
<Link
href="/"
className="shrink-0 bg-emerald-500 hover:bg-emerald-400 text-ink font-bold px-6 py-3.5 rounded-full text-sm shadow-xl hover:scale-105 transition-all duration-200"
>
Start Free Scan
</Link>
</div>
)}
</React.Fragment>
))}
{/* HowTo Step Guide */}
{post.howToSteps && post.howToSteps.length > 0 && (
<section
id="howto-step"
className="scroll-mt-28 bg-white p-6 sm:p-8 rounded-2xl border-2 border-emerald-600/20 shadow-md"
>
<div className="text-xs font-extrabold tracking-widest text-emerald-700 uppercase mb-2">
Action Plan
</div>
<h2 className="font-display text-2xl font-bold text-emerald-950 mb-6">
{post.howToName ?? 'Step-by-Step Action Guide'}
</h2>
<div className="space-y-4">
{post.howToSteps.map((step, idx) => (
<div
key={idx}
className="flex items-start gap-4 p-4.5 rounded-xl bg-emerald-50/50 border border-emerald-600/20"
>
<div className="w-8 h-8 rounded-full bg-emerald-600 text-white font-bold flex items-center justify-center shrink-0 text-sm shadow-sm">
{idx + 1}
</div>
<div>
<div className="font-bold text-emerald-950 text-base mb-1">{step.name}</div>
<div className="text-emerald-900/80 text-sm">{step.text}</div>
</div>
</div>
))}
</div>
</section>
)}
{/* FAQs */}
{post.faqs && post.faqs.length > 0 && (
<section id="faq" className="scroll-mt-28 pt-8 border-t-2 border-emerald-600/20">
<div className="text-xs font-extrabold tracking-widest text-emerald-700 uppercase mb-2">
Questions & Answers
</div>
<h2 className="font-display text-2xl sm:text-3xl font-bold text-emerald-950 mb-6">
Frequently Asked Questions
</h2>
<div className="space-y-4">
{post.faqs.map((faq, idx) => (
<div key={idx} className="p-6 rounded-2xl bg-white border border-emerald-600/20 shadow-xs">
<h3 className="font-bold text-base sm:text-lg text-emerald-950 mb-2">
{faq.question}
</h3>
<p className="text-emerald-900/80 text-sm sm:text-base leading-relaxed">
{faq.answer}
</p>
</div>
))}
</div>
</section>
)}
{/* App Banner */}
<div className="p-8 sm:p-10 rounded-3xl bg-gradient-to-br from-emerald-900 via-emerald-950 to-ink text-white border-2 border-emerald-400/30 text-center shadow-2xl my-12 relative overflow-hidden">
<div className="absolute top-0 right-0 w-64 h-64 bg-emerald-500/10 blur-3xl rounded-full pointer-events-none" />
<span className="text-xs font-extrabold tracking-widest uppercase text-emerald-400 block mb-2">
GreenLens Pro iOS App
</span>
<h3 className="font-display text-2xl sm:text-3xl font-bold mt-1 mb-3 text-white">
Identify Any Plant & Get Custom Care Plans
</h3>
<p className="text-white/80 text-sm sm:text-base max-w-[600px] mx-auto mb-6 leading-relaxed">
Scan your houseplants in seconds, diagnose leaf diseases, and get automatic watering reminders tailored to your space.
</p>
<a
href="https://apps.apple.com/app/greenlens"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 bg-emerald-500 hover:bg-emerald-400 text-ink font-bold px-7 py-3.5 rounded-full shadow-xl hover:scale-105 transition-all duration-200"
>
<span>Start First Scan Free No Account Needed</span>
</a>
</div>
</div>
</div>
{/* Sidebar */}
<aside className="lg:col-span-4 hidden lg:block">
<div className="sticky top-28 space-y-8">
{/* Table of Contents */}
{post.toc && post.toc.length > 0 && (
<div className="bg-white p-6 rounded-2xl border-2 border-emerald-600/20 shadow-sm">
<div className="font-extrabold text-xs uppercase tracking-widest text-emerald-800 mb-4">
Table of Contents
</div>
<nav className="space-y-2 text-sm font-medium">
{post.toc.map((item) => (
<a
key={item.id}
href={`#${item.id}`}
className="block text-emerald-900/70 hover:text-emerald-700 transition-colors py-1 border-l-2 border-transparent hover:border-emerald-600 pl-3"
>
{item.label}
</a>
))}
</nav>
</div>
)}
{/* Related Articles */}
{post.relatedPosts && post.relatedPosts.length > 0 && (
<div className="bg-white p-6 rounded-2xl border-2 border-emerald-600/20 shadow-sm">
<div className="font-extrabold text-xs uppercase tracking-widest text-emerald-800 mb-4">
Related Guides
</div>
<div className="space-y-4">
{post.relatedPosts.map((related) => (
<Link
key={related.slug}
href={`/blog/${related.slug}`}
className="group block p-3.5 rounded-xl hover:bg-emerald-50 border border-transparent hover:border-emerald-600/20 transition-all"
>
<div className="text-[10px] font-extrabold text-emerald-700 uppercase tracking-wider mb-1">
{related.category} {related.readTime}
</div>
<div className="font-bold text-sm text-emerald-950 group-hover:text-emerald-700 transition-colors line-clamp-2 leading-snug">
{related.title}
</div>
</Link>
))}
</div>
</div>
)}
</div>
</aside>
</div>
{/* Methodik & Quellen — Vertrauenssignal, bewusst am Ende und vollständig sichtbar */}
{(post.methodology || post.sources?.length) && (
<div className="max-w-[1320px] mx-auto px-6 mt-16 lg:pr-[calc(33.333%+3rem)]">
<section className="bg-white/80 border border-emerald-900/10 rounded-2xl p-7">
{post.methodology && (
<>
<h2 className="text-sm font-extrabold uppercase tracking-widest text-emerald-800 mb-3">
How this guide was compiled
</h2>
<p className="text-[15px] leading-relaxed text-emerald-950/80">{post.methodology}</p>
</>
)}
{post.sources && post.sources.length > 0 && (
<>
<h2 className="text-sm font-extrabold uppercase tracking-widest text-emerald-800 mt-7 mb-3">
Sources
</h2>
<ul className="space-y-2">
{post.sources.map((source) => (
<li key={source.url} className="text-[15px] leading-relaxed">
<a
href={source.url}
target="_blank"
rel="noopener noreferrer"
className="text-emerald-700 underline underline-offset-2 hover:text-emerald-900"
>
{source.title}
</a>
<span className="text-emerald-950/60"> {source.publisher}</span>
</li>
))}
</ul>
</>
)}
<p className="text-xs text-emerald-950/50 mt-6">
Published {post.publishedAt}
{post.updatedAt ? ` · Last updated ${post.updatedAt}` : ''} · Written by {post.author.name},{' '}
{post.author.role}
</p>
</section>
</div>
)}
</main>
{/* Site Footer */}
<SeoFooter locale="en" />
</div>
)
}

View File

@@ -177,7 +177,7 @@ export default function SeoFooter({ locale, showMethodology = true }: SeoFooterP
<li><a href="https://marketplace.visualstudio.com/items?itemName=TimoKnuth.greenlens-vscode" target="_blank" rel="noopener noreferrer" className="hover:text-white">VS Code Extension</a></li>
<li><a href="https://pypi.org/project/greenlens-python/" target="_blank" rel="noopener noreferrer" className="hover:text-white">Python SDK &amp; CLI (PyPI)</a></li>
<li><a href="https://www.npmjs.com/package/greenlenspro-cli" target="_blank" rel="noopener noreferrer" className="hover:text-white">Node.js CLI (NPM)</a></li>
<li><Link href="/developers" className="hover:text-white">Chrome Extension (Soon)</Link></li>
<li><a href={siteConfig.chromeWebStoreUrl} target="_blank" rel="noopener noreferrer" className="hover:text-white">Chrome Extension</a></li>
<li><a href="https://github.com/knuthtimo-lab/greenlenspro-cli#github-action" target="_blank" rel="noopener noreferrer" className="hover:text-white">GitHub Action</a></li>
<li><a href="https://github.com/knuthtimo-lab/greenlenspro-cli" target="_blank" rel="noopener noreferrer" className="hover:text-white">GitHub Repository</a></li>
</ul>

View File

@@ -74,8 +74,8 @@ export function SeoHero({
<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 className="font-display font-semibold text-sm text-primary mb-0.5">{profile.heroImageBadgeTitle ?? copy.hero.floatTitle}</div>
<div className="text-[10px] font-semibold tracking-[0.1em] uppercase text-on-surface-variant">{profile.heroImageBadgeLabel ?? copy.hero.floatLabel}</div>
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,11 @@ const routeGroups: Partial<Record<Lang, string>>[] = [
{ de: '/pflanzen-erkennen-kostenlos', en: '/best-plant-identification-app' },
{ en: '/plant-scanner', es: '/es/escaner-de-plantas' },
{ de: '/vs/google-lens', es: '/es/comparar/google-lens' },
// Ratgeber-Cluster: deutsche SEO-Seite und englischer Blogpost sind echte
// Übersetzungspaare, auch wenn die Templates unterschiedlich sind.
{ de: '/giessplan-zimmerpflanzen', en: '/blog/houseplant-watering-schedule' },
{ de: '/pflanzen-diagnose', en: '/blog/plant-health-diagnosis-guide' },
{ de: '/welche-pflanze-ist-das', en: '/blog/which-plant-is-this-guide' },
]
const fuzzyTargets: Record<string, Partial<Record<Lang, string>>> = {
@@ -68,7 +73,7 @@ const englishPaths = new Set([
export function getLocaleForPath(pathname: string): Lang {
if (pathname === '/es' || pathname.startsWith('/es/')) return 'es'
if (pathname === '/vs/google-lens') return 'de'
if (englishPaths.has(pathname) || pathname.startsWith('/vs/')) return 'en'
if (englishPaths.has(pathname) || pathname.startsWith('/vs/') || pathname.startsWith('/blog')) return 'en'
return 'de'
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,23 @@
const siteUrl = (process.env.NEXT_PUBLIC_SITE_URL || 'https://greenlenspro.com').trim()
export const siteConfig = {
name: 'GreenLens',
domain: siteUrl,
const siteUrl = (process.env.NEXT_PUBLIC_SITE_URL || 'https://greenlenspro.com').trim()
export const siteConfig = {
name: 'GreenLens',
domain: siteUrl,
supportEmail: 'timo@greenlenspro.com',
legalEmail: 'timo@greenlenspro.com',
iosAppStoreUrl: 'https://apps.apple.com/de/app/greenlens-pro/id6759843546?l=en-GB',
androidPlayStoreUrl: '',
company: {
legalName: 'GreenLens',
representative: 'Tim Knuth',
addressLine1: '',
addressLine2: '',
country: 'Germany',
registry: '',
vatId: '',
},
} as const
export const hasIosStoreUrl = siteConfig.iosAppStoreUrl.trim().length > 0
export const hasAndroidStoreUrl = siteConfig.androidPlayStoreUrl.trim().length > 0
iosAppStoreUrl: 'https://apps.apple.com/de/app/greenlens-pro/id6759843546?l=en-GB',
androidPlayStoreUrl: '',
chromeWebStoreUrl: 'https://chromewebstore.google.com/detail/mhjlfgkdckkimpjeifippefjhalcafjg',
company: {
legalName: 'GreenLens',
representative: 'Tim Knuth',
addressLine1: '',
addressLine2: '',
country: 'Germany',
registry: '',
vatId: '',
},
} as const
export const hasIosStoreUrl = siteConfig.iosAppStoreUrl.trim().length > 0
export const hasAndroidStoreUrl = siteConfig.androidPlayStoreUrl.trim().length > 0

View File

@@ -89,6 +89,11 @@ export const spanishSeoPageProfiles: Record<string, SeoPageProfile> = {
label: 'Diagnosticar enfermedades de plantas',
description: 'Analiza sintomas y recibe el siguiente paso mas seguro.',
},
{
href: '/es/como-funciona-el-reconocimiento-de-plantas-por-ia',
label: 'Como funciona el reconocimiento de plantas por IA',
description: 'Entiende que hay detras del escaneo antes de confiar en el resultado.',
},
],
},
@@ -175,6 +180,11 @@ export const spanishSeoPageProfiles: Record<string, SeoPageProfile> = {
label: 'Diagnostico de plantas',
description: 'Cuando una planta muestra sintomas, revisa la causa probable.',
},
{
href: '/es/plantas-de-interior-poca-luz',
label: 'Plantas de interior con poca luz',
description: 'Elige especies que toleren el rincon mas oscuro de tu casa.',
},
],
},
@@ -436,6 +446,369 @@ export const spanishSeoPageProfiles: Record<string, SeoPageProfile> = {
},
],
},
'como-funciona-el-reconocimiento-de-plantas-por-ia': {
slug: 'como-funciona-el-reconocimiento-de-plantas-por-ia',
locale: 'es',
metaTitle: '¿Cómo Funciona Realmente el Reconocimiento de Plantas por IA?',
metaDescription:
'¿Cómo identifica una app la especie de una planta a partir de una sola foto? Explicamos cómo funciona la IA detrás del reconocimiento de plantas, qué tan precisa es realmente y cómo sacar el mejor resultado en cada escaneo.',
canonical: '/es/como-funciona-el-reconocimiento-de-plantas-por-ia',
h1: '¿Cómo Funciona Realmente el Reconocimiento de Plantas por IA?',
tagline: 'La IA acierta el nombre en segundos. Lo dificil es saber cuando conviene dudar de ella.',
directAnswer:
'Las apps de reconocimiento de plantas usan redes neuronales convolucionales entrenadas con millones de fotos etiquetadas para comparar tu imagen con miles de especies y devolver una lista de coincidencias con un porcentaje de confianza. Con una foto nitida de una especie comun, los modelos bien entrenados superan el 95% de precision.',
definitionBlock:
'El reconocimiento de plantas por IA usa una rama del aprendizaje profundo llamada Redes Neuronales Convolucionales (CNN). El modelo detecta y pondera caracteristicas visuales como la forma de la hoja, el margen, la venacion, la textura y la estructura floral, y las compara contra millones de imagenes de entrenamiento para devolver las especies mas probables.',
lastUpdated: 'Agosto 2026',
lastUpdatedIso: '2026-08-05',
includeAppSchema: true,
heroImage: '/hero-image.png',
heroImageAlt:
'Persona fotografiando una hoja con el movil para identificarla mediante inteligencia artificial',
contentSections: [
{
eyebrow: 'Como funciona',
title: 'Que pasa en los dos segundos tras la foto',
body:
'La IA no "ve" la planta como tu. Detecta y pondera cientos de caracteristicas visuales de forma simultanea, entrenadas sobre imagenes de jardines botanicos, bases de datos de ciencia ciudadana como iNaturalist y herbarios digitales.',
bullets: [
'Forma de la hoja: lobulada, ovalada, lanceolada, palmeada, lineal.',
'Margen foliar: liso, serrado, ondulado, dentado.',
'Venacion: pinnada, palmeada, paralela.',
'Textura superficial, distribucion del color y estructura floral, si existe.',
'El modelo devuelve una lista de las 3 a 5 especies mas probables, cada una con un porcentaje de confianza.',
],
},
{
eyebrow: 'Gratis vs. de pago',
title: 'Que suele incluir cada nivel',
body:
'La identificacion gratuita cubre la mayoria de los usos cotidianos. Esto es lo que suele estar en cada nivel, en la categoria en general.',
bullets: [
'Gratis: nombre comun y cientifico, resumen basico de cuidados, aviso de toxicidad y acceso a foros.',
'De pago: diagnostico de enfermedades por foto, escaneos ilimitados, identificacion detallada de plagas, diario de crecimiento y modo sin conexion.',
],
},
{
eyebrow: 'Casos de uso',
title: '5 situaciones donde esta tecnologia marca la diferencia',
body: 'Del regalo sin etiqueta a la compra en el vivero, estas son las situaciones donde escanear ahorra tiempo y evita errores.',
bullets: [
'La planta misteriosa: sin nombre ni etiqueta, el escaneo te da la especie y sus necesidades basicas al instante.',
'"¿Es venenosa?": consulta la ficha de toxicidad de inmediato, aunque conviene llamar siempre al centro de toxicologia ante cualquier duda real.',
'Identificacion en rutas y naturaleza: ayuda a distinguir bayas o plantas invasoras, pero verifica siempre con una segunda fuente antes de tocar o consumir algo silvestre.',
'Compras mas inteligentes en el vivero: escanea el ejemplar del mostrador antes de comprarlo para saber si se adapta a tu espacio.',
'Aprender con los ninos: convierte un paseo en un juego de identificacion natural.',
],
},
{
eyebrow: 'Consejos practicos',
title: 'Como hacer la foto perfecta para maxima precision',
body: 'La precision es cosa de dos: la IA solo puede trabajar con lo que le das.',
bullets: [
'Fotografia una sola hoja bien desarrollada, con luz natural y sin flash, llenando al menos el 50% del encuadre.',
'Un fondo blanco liso y una flor visible, si la hay, mejoran mucho la precision.',
'Evita vistas aereas de toda la planta, contraluz, plantulas muy jovenes, capturas de pantalla y hojas muy enfermas o necroticas.',
'Si la confianza es baja, prueba con el enves de la hoja, una seccion del tallo o el cepellon si la planta esta recien trasplantada.',
],
},
{
eyebrow: 'Comparativa',
title: 'Reconocimiento por IA vs. Google Lens',
body: 'Google Lens tambien identifica plantas, y es gratis. La diferencia esta en lo que ocurre despues del nombre.',
bullets: [
'Precision de especie: Google Lens usa conocimiento general; una app dedicada usa un conjunto de datos botanico especializado.',
'Instrucciones de cuidado y diagnostico de enfermedades: ausentes en Google Lens, disponibles justo despues del escaneo en una app especializada.',
'Comprobacion de toxicidad: indirecta via busqueda web en Google Lens, directa en los resultados de una app dedicada.',
'Si solo quieres un nombre ocasional, Google Lens basta. Si te importan tus plantas, una app especializada aporta mucho mas.',
],
},
{
eyebrow: 'Limites',
title: 'Las plantas mas dificiles de identificar',
body: 'Ninguna app es perfecta. Estas son las situaciones donde incluso la mejor tecnologia tiene dificultades.',
bullets: [
'Cultivares variegados: escanea la hoja mas "normal" de la planta.',
'Plantulas: espera hasta que aparezcan las primeras hojas verdaderas.',
'Especies muy raras: usa iNaturalist para identificacion comunitaria.',
'Hojas muy enfermas: fotografia una hoja sana si esta disponible.',
'Suculentas y cactus de aspecto similar: fotografia desde multiples angulos.',
],
},
{
eyebrow: 'Siguiente frontera',
title: 'Diagnostico de enfermedades de plantas',
body: 'Identificar la especie es solo el comienzo. Las apps avanzadas tambien analizan sintomas visibles para sugerir la causa mas probable.',
bullets: [
'Hojas amarillas: exceso de riego, falta de agua, deficiencia nutricional o dano por plagas.',
'Puntas marrones: baja humedad, toxicidad por fluor o falta de riego.',
'Polvo blanco: oidio (hongos) o depositos minerales del agua dura.',
'Residuo pegajoso: melaza de pulgones o cochinillas.',
],
},
],
featureTable: {
title: 'Que distingue a GreenLens de un reconocimiento de plantas generico',
alternativeLabel: 'Apps de identificacion genericas',
rows: [
{
feature: 'Precision por especie',
greenlens: 'Modelo apoyado en un conjunto de datos botanico especializado, con buen rendimiento en especies comunes.',
alternative: 'Motores de proposito general con menos precision en flora.',
},
{
feature: 'Despues del escaneo',
greenlens: 'Conecta el resultado con cuidado, riego y diagnostico de salud en el mismo flujo.',
alternative: 'Suele terminar en el nombre de la especie.',
},
{
feature: 'Catalogo de cuidado',
greenlens: 'Fichas verificadas a mano para unas 240 especies, en crecimiento cada mes.',
alternative: 'Presume reconocer miles de especies, pero sin fichas de cuidado verificadas.',
},
],
},
greenLensIf: [
'Quieres saber que planta tienes delante y como cuidarla, no solo su nombre.',
'Te interesa entender que hace la IA detras del escaneo antes de confiar en el resultado.',
'Buscas una segunda opinion rapida antes de comprar una planta en el vivero.',
],
notBestIf: [
'Necesitas identificacion botanica profesional certificada para fines cientificos.',
'Quieres verificar si una planta silvestre es segura para comer — consulta siempre una guia de campo o un experto, nunca solo una app.',
],
offer: {
eyebrow: 'Pruebalo primero',
title: 'Escanea y compruebalo tu mismo',
body:
'Fotografia una hoja y mira lo que recibes antes de decidir nada: nombre de la especie, cuidados y una revision de salud si la planta muestra sintomas. Sin cuenta y sin datos de pago.',
terms:
'Un escaneo gratis, sin crear cuenta. Tres mas despues de registrarte. A partir de ahi necesitas GreenLens Pro: suscripcion mensual o anual, con los precios actuales en la App Store. Se renueva automaticamente y puedes cancelar cuando quieras desde los ajustes del iPhone.',
ctaLabel: 'Haz tu primer escaneo gratis — sin cuenta',
},
faqs: [
{
question: '¿Qué apps ofrecen la mejor identificación gratuita de plantas?',
answer:
'GreenLens Pro, PlantNet e iNaturalist ofrecen una identificación gratuita potente. La precisión depende mucho de la calidad de la foto.',
},
{
question: '¿Pueden las apps identificar plantas a partir de una foto de mi galería?',
answer:
'Sí. Todas las principales apps de identificación de plantas aceptan imágenes de tu galería, no solo fotos tomadas en directo.',
},
{
question: '¿Cuántas plantas puede reconocer un identificador de plantas por IA?',
answer:
'Los motores genéricos presumen de reconocer entre 10.000 y 400.000+ especies a nivel básico, pero reconocer la forma de una hoja no es lo mismo que saber cómo mantener esa planta con vida. GreenLens ha construido a mano fichas de cuidado detalladas y comprobadas para unas 240 especies hasta ahora, y añade especies nuevas cada mes.',
},
{
question: '¿Es seguro usar una app para identificar plantas silvestres comestibles?',
answer:
'Como primer paso, sí. Pero nunca consumas nada basándote únicamente en la identificación de una app: verifica siempre con una guía de campo o un experto.',
},
{
question: '¿Funcionan las apps identificadoras de plantas sin conexión?',
answer:
'Algunas ofrecen modo sin conexión limitado para las especies más comunes. La cobertura completa normalmente requiere conexión a internet.',
},
],
relatedLinks: [
{
href: '/es/identificador-de-plantas',
label: 'Identificador de plantas',
description: 'Escanea una planta y recibe su nombre junto con el plan de cuidado.',
},
{
href: '/es/diagnosticar-enfermedades-plantas',
label: 'Diagnosticar enfermedades de plantas',
description: 'Cuando el problema no es el nombre sino un sintoma, empieza aqui.',
},
{
href: '/es/plantas-de-interior-poca-luz',
label: 'Plantas de interior con poca luz',
description: 'Descubre que especies toleran rincones oscuros y como evaluarlo sin aparatos.',
},
],
},
'plantas-de-interior-poca-luz': {
slug: 'plantas-de-interior-poca-luz',
locale: 'es',
metaTitle: 'Plantas de interior con poca luz — guia completa',
metaDescription:
'¿Qué plantas de interior necesitan poca luz? Descubre las 10 plantas más resistentes para habitaciones oscuras, sombra y pasillos, con consejos para evaluar la luz de tu casa.',
canonical: '/es/plantas-de-interior-poca-luz',
h1: 'Las 10 Mejores Plantas de Interior con Poca Luz',
tagline: 'No toda casa tiene ventanales al sur — estas plantas no los necesitan.',
directAnswer:
'Zamioculca, sansevieria, aspidistra, potos y espatifilo estan entre las plantas de interior mas tolerantes a la poca luz: heredaron del sotobosque tropical la capacidad de funcionar con tasas de fotosintesis reducidas y sobrevivir con 300-800 lux, muy por debajo de los 2.000+ lux que necesitan la mayoria de las plantas de interior comunes.',
definitionBlock:
'Poca luz no significa sin luz: toda planta verde necesita fotones para hacer fotosintesis. En botanica la intensidad se mide en lux, y las plantas tolerantes a sombra evolucionaron bajo el dosel de los bosques tropicales para funcionar con 300-800 lux, un nivel mucho mas bajo que la luz indirecta brillante.',
lastUpdated: 'Agosto 2026',
lastUpdatedIso: '2026-08-05',
includeAppSchema: true,
heroImage: '/hero-plant.png',
heroImageAlt:
'Rincon de una habitacion con poca luz natural decorado con plantas de interior resistentes a la sombra',
contentSections: [
{
eyebrow: 'Conceptos basicos',
title: 'Que significa realmente "poca luz" para las plantas',
body: 'Un error comun en jardineria de interior es pensar que "poca luz" equivale a "sin luz". Estos son los niveles orientativos, de mas a menos intensidad.',
bullets: [
'Luz indirecta brillante: mas de 2.000 lux — suculentas, ficus, cactus.',
'Sombra media: 1.000-2.000 lux — monstera, potos, filodendro.',
'Poca luz (tolerantes a sombra): 300-800 lux — sansevieria, zamioculca, cuna de Moises, aspidistra.',
'Menos de 300 lux: sin una lampara de crecimiento LED, incluso las plantas mas resistentes terminan debilitandose.',
],
},
{
eyebrow: 'El top 10',
title: 'Las 10 mejores plantas de interior para poca luz',
body: 'De la zamioculca a la cinta, estas diez especies estan entre las mas resistentes a la sombra para interiores.',
bullets: [
'Zamioculca (Zamioculcas zamiifolia): 300-600 lux. Riega muy poco, en zonas sombrias cada 4 a 6 semanas.',
'Sansevieria / lengua de suegra (Dracaena trifasciata): 400-800 lux. Evita el exceso de agua para prevenir pudricion de raices.',
'Aspidistra (Aspidistra elatior): 300-700 lux. Ideal para pasillos frios y entradas sin sol directo.',
'Potos (Epipremnum aureum): 500-1.000 lux. Excelente para estantes altos o macetas colgantes en zonas sombrias.',
'Espatifilo / cuna de Moises (Spathiphyllum): 400-800 lux. Avisa que necesita agua inclinando sus hojas.',
'Aglaonema: 500-800 lux. Las variedades verde oscuro toleran mejor la sombra que las rosadas o rojas.',
'Palmera de salon (Chamaedorea elegans): 600-1.000 lux. Pulveriza sus hojas para mantener la humedad.',
'Filodendro hoja de corazon (Philodendron hederaceum): 500-900 lux. Deja secar la capa superior de tierra entre riegos.',
'Calatea (Calathea / Goeppertia): 600-1.000 lux. No tolera el sol directo pero exige alta humedad.',
'Cinta / mala madre (Chlorophytum comosum): 500-1.200 lux. Muy adaptable y facil de reproducir por hijuelos.',
],
},
{
eyebrow: 'Riego rapido',
title: 'Luz y riego de un vistazo',
body: 'Un resumen de la frecuencia de riego en verano e invierno para las especies mas populares de esta lista.',
bullets: [
'Zamioculca (300-600 lux): cada 3 semanas en verano, cada 5-6 semanas en invierno.',
'Sansevieria (400-800 lux): cada 2-3 semanas en verano, cada 4 semanas en invierno.',
'Aspidistra (300-700 lux): cada 2 semanas en verano, cada 3-4 semanas en invierno.',
'Potos (500-1.000 lux): semanal en verano, cada 2 semanas en invierno.',
'Espatifilo (400-800 lux): semanal en verano, cada 1-2 semanas en invierno.',
],
},
{
eyebrow: 'Senales de alerta',
title: 'Sintomas de falta de luz: como detectarla',
body: 'Cuando una planta recibe menos luz de la que necesita, muestra senales claras.',
bullets: [
'Etiolacion: tallos alargados y debiles con gran espacio entre hojas, buscando la fuente de luz.',
'Perdida de variegacion: las hojas veteadas se vuelven verde oscuro solido para maximizar la clorofila.',
'Crecimiento estancado: la planta no produce hojas nuevas durante meses.',
'Hojas amarillas o pudricion: la fotosintesis se ralentiza, la planta consume menos agua y el riego excesivo pudre las raices con mas facilidad.',
],
},
{
eyebrow: 'Sin aparatos',
title: 'Como evaluar la luz de tu casa sin ningun aparato',
body: 'No necesitas un luxometro: con un poco de observacion puedes averiguarlo tu mismo.',
bullets: [
'La prueba de la sombra: coloca la mano a 30 cm sobre la superficie al mediodia y observa la nitidez de la sombra.',
'La orientacion de la ventana importa: sur da sol directo todo el dia, este luz matutina suave, oeste sol intenso por la tarde y norte la luz mas constante y suave, a menudo ideal para plantas tolerantes a sombra.',
'La distancia a la ventana es tan importante como la orientacion: un rincon a 2 metros de una ventana luminosa ya puede estar en el rango de poca luz.',
'La luz cambia con las estaciones: un lugar con luz media en verano puede convertirse en poca luz en invierno, asi que observa tus plantas durante el ano.',
],
},
],
featureTable: {
title: 'Que aporta GreenLens a la hora de elegir planta segun la luz',
alternativeLabel: 'Buscar por tu cuenta',
rows: [
{
feature: 'Requisito de luz por especie',
greenlens: 'Cada ficha del catalogo indica si la planta necesita poca luz, media/indirecta, indirecta brillante o sol directo.',
alternative: 'Hay que buscar y contrastar varias fuentes para cada especie.',
},
{
feature: 'Antes de comprar',
greenlens: 'Escanea la planta en el vivero y comprueba si encaja con el rincon que tienes en mente.',
alternative: 'Sueles decidir en la tienda sin verificar el requisito real de luz.',
},
{
feature: 'Catalogo de cuidado',
greenlens: 'Unas 240 especies con fichas verificadas a mano, en crecimiento cada mes.',
alternative: 'Bases de datos genericas sin verificacion de cuidado real.',
},
],
},
greenLensIf: [
'Tienes un rincon oscuro y no sabes que planta sobrevivira ahi.',
'Quieres comprobar el requisito de luz de una planta antes de comprarla.',
'Ya tienes plantas y sospechas que la falta de luz esta detras de su estancamiento.',
],
notBestIf: [
'Buscas exclusivamente plantas de sol directo o de exterior.',
'Necesitas mediciones exactas de lux con un luxometro profesional.',
],
offer: {
eyebrow: 'Antes de comprar',
title: 'Comprueba el requisito de luz antes de decidir',
body:
'Escanea o busca la planta en el catalogo de GreenLens y consulta su requisito real de luz antes de llevarla a un rincon donde no prosperaria.',
terms:
'Un escaneo gratis, sin crear cuenta. Tres mas despues de registrarte. A partir de ahi necesitas GreenLens Pro: suscripcion mensual o anual, con los precios actuales en la App Store. Se renueva automaticamente y puedes cancelar cuando quieras desde los ajustes del iPhone.',
ctaLabel: 'Haz tu primer escaneo gratis — sin cuenta',
},
howToName: 'Como evaluar la luz de tu casa sin aparatos',
howToSteps: [
{
name: 'Haz la prueba de la sombra',
text: 'Coloca la mano a 30 cm sobre la superficie donde iria la planta, al mediodia: sombra nitida es luz brillante, sombra suave es luz media, sombra apenas perceptible es poca luz, y sin sombra visible es demasiado oscuro sin una lampara LED.',
},
{
name: 'Revisa la orientacion de la ventana',
text: 'Sur recibe sol directo todo el dia, este da luz matutina suave, oeste trae sol intenso por la tarde y norte ofrece la luz mas constante y suave, a menudo la mejor opcion para plantas tolerantes a la sombra.',
},
{
name: 'Mide la distancia a la ventana',
text: 'La intensidad lumínica cae rapido cuanto mas te alejas: un rincon a 2 metros de una ventana luminosa ya puede estar en el rango de poca luz.',
},
{
name: 'Ten en cuenta la estacion del ano',
text: 'Un lugar con luz media en verano puede convertirse en poca luz en invierno porque el sol esta mas bajo y los dias son mas cortos; acerca tus plantas a la ventana si hace falta.',
},
],
faqs: [
{
question: '¿Puede sobrevivir una planta en un baño o pasillo sin ventanas?',
answer:
'Ninguna planta verde sobrevive sin luz a largo plazo. En habitaciones sin ventanas debes utilizar lámparas de crecimiento LED (300+ lux durante 8-10 horas al día).',
},
{
question: '¿Por qué se secan las puntas de las hojas en invierno?',
answer:
'Las puntas marrones suelen deberse a la baja humedad por la calefacción o al uso de agua de grifo con cloro o cal.',
},
{
question: '¿Debo regar menos las plantas que están en la sombra?',
answer:
'Sí. En zonas con poca luz las plantas consumen agua mucho más despacio. Comprueba siempre los primeros 3 cm de tierra antes de volver a regar.',
},
],
relatedLinks: [
{
href: '/es/como-funciona-el-reconocimiento-de-plantas-por-ia',
label: 'Cómo funciona el reconocimiento de plantas por IA',
description: 'Entiende que hay detras del escaneo antes de confiar en el resultado.',
},
{
href: '/es/identificador-de-plantas',
label: 'Identificador de plantas',
description: 'Identifica la especie y consulta su requisito de luz en el mismo escaneo.',
},
{
href: '/es/app-para-cuidar-plantas',
label: 'App para cuidar plantas',
description: 'Organiza el riego y la ubicacion de cada planta segun sus necesidades.',
},
],
},
}
export const spanishSeoPageSlugs = Object.keys(spanishSeoPageProfiles)

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 KiB