SEO blogpost
This commit is contained in:
59
greenlns-landing/app/blog/[slug]/page.tsx
Normal file
59
greenlns-landing/app/blog/[slug]/page.tsx
Normal 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} />
|
||||
}
|
||||
@@ -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} />
|
||||
}
|
||||
97
greenlns-landing/app/blog/page.tsx
Normal file
97
greenlns-landing/app/blog/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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} />
|
||||
}
|
||||
22
greenlns-landing/app/blog/which-plant-is-this-guide/page.tsx
Normal file
22
greenlns-landing/app/blog/which-plant-is-this-guide/page.tsx
Normal 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} />
|
||||
}
|
||||
Reference in New Issue
Block a user