feat(blog): resolve SEO cannibalization, update blog posts & clean up dev posts
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React from 'react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import SeoNavbar from '@/components/seo/SeoNavbar'
|
||||
import SeoFooter from '@/components/seo/SeoFooter'
|
||||
@@ -6,6 +7,84 @@ import { BlogPost } from '@/lib/blogPosts'
|
||||
|
||||
const SITE = 'https://greenlenspro.com'
|
||||
|
||||
const blogUiCopy = {
|
||||
en: {
|
||||
home: 'Home',
|
||||
blog: 'Blog',
|
||||
quickAnswer: 'Quick Botanical Answer',
|
||||
tableOfContents: 'Table of Contents',
|
||||
updated: 'Updated',
|
||||
actionPlan: 'Action Plan',
|
||||
stepByStepGuide: 'Step-by-Step Action Guide',
|
||||
questionsAndAnswers: 'Questions & Answers',
|
||||
faqHeading: 'Frequently Asked Questions',
|
||||
relatedGuides: 'Related Guides',
|
||||
ctaEyebrow: 'Instant AI Health Diagnosis',
|
||||
ctaTitle: 'Unsure what is wrong with your plant?',
|
||||
ctaBody: 'Scan your plant leaf with GreenLens AI for an immediate disease check, pest identification, and custom care plan.',
|
||||
ctaButton: 'Start Free Scan ➔',
|
||||
appEyebrow: 'GreenLens Pro iOS App',
|
||||
appTitle: 'Identify Any Plant & Get Custom Care Plans',
|
||||
appBody: 'Scan your houseplants in seconds, diagnose leaf diseases, and get automatic watering reminders tailored to your space.',
|
||||
appButton: 'Start First Scan Free — No Account Needed',
|
||||
howCompiled: 'How this guide was compiled',
|
||||
sources: 'Sources',
|
||||
published: 'Published',
|
||||
lastUpdated: 'Last updated',
|
||||
writtenBy: 'Written by',
|
||||
},
|
||||
de: {
|
||||
home: 'Start',
|
||||
blog: 'Blog',
|
||||
quickAnswer: 'Kurze Antwort',
|
||||
tableOfContents: 'Inhaltsverzeichnis',
|
||||
updated: 'Aktualisiert am',
|
||||
actionPlan: 'Handlungsplan',
|
||||
stepByStepGuide: 'Schritt-für-Schritt-Anleitung',
|
||||
questionsAndAnswers: 'Fragen & Antworten',
|
||||
faqHeading: 'Häufig gestellte Fragen',
|
||||
relatedGuides: 'Weitere Ratgeber',
|
||||
ctaEyebrow: 'Sofortige KI-Gesundheitsanalyse',
|
||||
ctaTitle: 'Unsicher, was mit deiner Pflanze los ist?',
|
||||
ctaBody: 'Scanne ein Blatt deiner Pflanze mit GreenLens AI für eine sofortige Diagnose, Schädlingserkennung und einen individuellen Pflegeplan.',
|
||||
ctaButton: 'Kostenlosen Scan starten ➔',
|
||||
appEyebrow: 'GreenLens Pro iOS App',
|
||||
appTitle: 'Jede Pflanze erkennen & individuelle Pflegepläne erhalten',
|
||||
appBody: 'Scanne deine Zimmerpflanzen in Sekunden, diagnostiziere Blattkrankheiten und erhalte automatische Gießerinnerungen, abgestimmt auf deinen Standort.',
|
||||
appButton: 'Ersten Scan gratis starten — ohne Anmeldung',
|
||||
howCompiled: 'Wie dieser Ratgeber entstanden ist',
|
||||
sources: 'Quellen',
|
||||
published: 'Veröffentlicht am',
|
||||
lastUpdated: 'Zuletzt aktualisiert am',
|
||||
writtenBy: 'Verfasst von',
|
||||
},
|
||||
es: {
|
||||
home: 'Inicio',
|
||||
blog: 'Blog',
|
||||
quickAnswer: 'Respuesta rápida',
|
||||
tableOfContents: 'Índice de contenidos',
|
||||
updated: 'Actualizado',
|
||||
actionPlan: 'Plan de acción',
|
||||
stepByStepGuide: 'Guía paso a paso',
|
||||
questionsAndAnswers: 'Preguntas y respuestas',
|
||||
faqHeading: 'Preguntas frecuentes',
|
||||
relatedGuides: 'Guías relacionadas',
|
||||
ctaEyebrow: 'Diagnóstico instantáneo con IA',
|
||||
ctaTitle: '¿No sabes qué le pasa a tu planta?',
|
||||
ctaBody: 'Escanea una hoja de tu planta con GreenLens AI para un diagnóstico inmediato, detección de plagas y un plan de cuidado personalizado.',
|
||||
ctaButton: 'Iniciar escaneo gratis ➔',
|
||||
appEyebrow: 'GreenLens Pro para iOS',
|
||||
appTitle: 'Identifica cualquier planta y recibe planes de cuidado personalizados',
|
||||
appBody: 'Escanea tus plantas de interior en segundos, diagnostica enfermedades de las hojas y recibe recordatorios de riego automáticos adaptados a tu espacio.',
|
||||
appButton: 'Haz tu primer escaneo gratis — sin cuenta',
|
||||
howCompiled: 'Cómo se elaboró esta guía',
|
||||
sources: 'Fuentes',
|
||||
published: 'Publicado el',
|
||||
lastUpdated: 'Última actualización',
|
||||
writtenBy: 'Escrito por',
|
||||
},
|
||||
} as const
|
||||
|
||||
/**
|
||||
* JSON-LD für Blogposts. BlogPosting liefert Autor und Datum, FAQPage und HowTo
|
||||
* sind die beiden Formate, aus denen AI-Antwortsysteme direkt extrahieren.
|
||||
@@ -116,6 +195,8 @@ function renderFormattedText(text: string) {
|
||||
}
|
||||
|
||||
export function BlogPostTemplate({ post }: { post: BlogPost }) {
|
||||
const t = blogUiCopy[post.locale] ?? blogUiCopy.en
|
||||
|
||||
return (
|
||||
<div className="bg-[#fdfbf6] text-on-surface font-body antialiased min-h-screen relative overflow-hidden">
|
||||
<script
|
||||
@@ -124,7 +205,7 @@ export function BlogPostTemplate({ post }: { post: BlogPost }) {
|
||||
/>
|
||||
|
||||
{/* Navigation Header */}
|
||||
<SeoNavbar locale="en" />
|
||||
<SeoNavbar locale={post.locale} />
|
||||
|
||||
{/* Decorative Green Glow Blobs */}
|
||||
<div className="absolute inset-0 pointer-events-none -z-10" aria-hidden="true">
|
||||
@@ -141,11 +222,11 @@ export function BlogPostTemplate({ post }: { post: BlogPost }) {
|
||||
{/* 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
|
||||
{t.home}
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<Link href="/blog" className="hover:text-emerald-700 transition-colors">
|
||||
Blog
|
||||
{t.blog}
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span className="text-emerald-700 font-bold">{post.category}</span>
|
||||
@@ -177,7 +258,7 @@ export function BlogPostTemplate({ post }: { post: BlogPost }) {
|
||||
<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}
|
||||
{post.author.role} • {t.updated} {post.publishedAt}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -190,11 +271,13 @@ export function BlogPostTemplate({ post }: { post: BlogPost }) {
|
||||
<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
|
||||
<Image
|
||||
src={post.heroImage}
|
||||
alt={post.heroImageAlt}
|
||||
className="w-full h-full object-cover"
|
||||
fill
|
||||
priority
|
||||
sizes="(min-width: 1024px) 66vw, 100vw"
|
||||
className="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">
|
||||
@@ -214,7 +297,7 @@ export function BlogPostTemplate({ post }: { post: BlogPost }) {
|
||||
{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>
|
||||
<span>⚡ {t.quickAnswer}</span>
|
||||
</div>
|
||||
<p className="text-emerald-950 font-medium leading-relaxed text-base max-w-[72ch]">
|
||||
{renderFormattedText(post.directAnswer)}
|
||||
@@ -226,7 +309,7 @@ export function BlogPostTemplate({ post }: { post: BlogPost }) {
|
||||
{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
|
||||
{t.tableOfContents}
|
||||
</div>
|
||||
<ul className="space-y-2.5 text-sm font-medium">
|
||||
{post.toc.map((item) => (
|
||||
@@ -322,20 +405,20 @@ export function BlogPostTemplate({ post }: { post: BlogPost }) {
|
||||
<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>
|
||||
<span>🌿 {t.ctaEyebrow}</span>
|
||||
</div>
|
||||
<h3 className="font-display text-xl sm:text-2xl font-bold text-white">
|
||||
Unsure what is wrong with your plant?
|
||||
{t.ctaTitle}
|
||||
</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.
|
||||
{t.ctaBody}
|
||||
</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 ➔
|
||||
{t.ctaButton}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
@@ -349,10 +432,10 @@ export function BlogPostTemplate({ post }: { post: BlogPost }) {
|
||||
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
|
||||
{t.actionPlan}
|
||||
</div>
|
||||
<h2 className="font-display text-2xl font-bold text-emerald-950 mb-6">
|
||||
{post.howToName ?? 'Step-by-Step Action Guide'}
|
||||
{post.howToName ?? t.stepByStepGuide}
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
{post.howToSteps.map((step, idx) => (
|
||||
@@ -377,10 +460,10 @@ export function BlogPostTemplate({ post }: { post: BlogPost }) {
|
||||
{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
|
||||
{t.questionsAndAnswers}
|
||||
</div>
|
||||
<h2 className="font-display text-2xl sm:text-3xl font-bold text-emerald-950 mb-6">
|
||||
Frequently Asked Questions
|
||||
{t.faqHeading}
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
{post.faqs.map((faq, idx) => (
|
||||
@@ -401,13 +484,13 @@ export function BlogPostTemplate({ post }: { post: BlogPost }) {
|
||||
<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
|
||||
{t.appEyebrow}
|
||||
</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
|
||||
{t.appTitle}
|
||||
</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.
|
||||
{t.appBody}
|
||||
</p>
|
||||
<a
|
||||
href="https://apps.apple.com/app/greenlens"
|
||||
@@ -415,7 +498,7 @@ export function BlogPostTemplate({ post }: { post: BlogPost }) {
|
||||
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>
|
||||
<span>{t.appButton}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -448,7 +531,7 @@ export function BlogPostTemplate({ post }: { post: BlogPost }) {
|
||||
{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
|
||||
{t.relatedGuides}
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{post.relatedPosts.map((related) => (
|
||||
@@ -479,7 +562,7 @@ export function BlogPostTemplate({ post }: { post: BlogPost }) {
|
||||
{post.methodology && (
|
||||
<>
|
||||
<h2 className="text-sm font-extrabold uppercase tracking-widest text-emerald-800 mb-3">
|
||||
How this guide was compiled
|
||||
{t.howCompiled}
|
||||
</h2>
|
||||
<p className="text-[15px] leading-relaxed text-emerald-950/80">{post.methodology}</p>
|
||||
</>
|
||||
@@ -488,7 +571,7 @@ export function BlogPostTemplate({ post }: { post: BlogPost }) {
|
||||
{post.sources && post.sources.length > 0 && (
|
||||
<>
|
||||
<h2 className="text-sm font-extrabold uppercase tracking-widest text-emerald-800 mt-7 mb-3">
|
||||
Sources
|
||||
{t.sources}
|
||||
</h2>
|
||||
<ul className="space-y-2">
|
||||
{post.sources.map((source) => (
|
||||
@@ -509,8 +592,8 @@ export function BlogPostTemplate({ post }: { post: BlogPost }) {
|
||||
)}
|
||||
|
||||
<p className="text-xs text-emerald-950/50 mt-6">
|
||||
Published {post.publishedAt}
|
||||
{post.updatedAt ? ` · Last updated ${post.updatedAt}` : ''} · Written by {post.author.name},{' '}
|
||||
{t.published} {post.publishedAt}
|
||||
{post.updatedAt ? ` · ${t.lastUpdated} ${post.updatedAt}` : ''} · {t.writtenBy} {post.author.name},{' '}
|
||||
{post.author.role}
|
||||
</p>
|
||||
</section>
|
||||
@@ -519,7 +602,7 @@ export function BlogPostTemplate({ post }: { post: BlogPost }) {
|
||||
</main>
|
||||
|
||||
{/* Site Footer */}
|
||||
<SeoFooter locale="en" />
|
||||
<SeoFooter locale={post.locale} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user