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')