QR-master/src/app/(marketing)/blog/[slug]/page.tsx

215 lines
8.4 KiB
TypeScript

import React from 'react';
import type { Metadata } from 'next';
import Link from 'next/link';
import Image from 'next/image';
import { notFound, permanentRedirect } from 'next/navigation';
import SeoJsonLd from '@/components/SeoJsonLd';
import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
import { blogPostingSchema, breadcrumbSchema, howToSchema } from '@/lib/schema';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { blogPosts, type BlogPostData } from '@/lib/blog-data';
export function generateStaticParams() {
return Object.keys(blogPosts).map((slug) => ({
slug,
}));
}
export function generateMetadata({ params }: { params: { slug: string } }): Metadata {
// Prevent soft 404s for missing assets in metadata generation
if (params.slug.match(/\.(png|jpg|jpeg|gif|svg|ico|json|txt|xml)$/i)) {
return notFound();
}
const post = blogPosts[params.slug];
if (!post) {
return notFound();
}
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: 'article',
publishedTime: post.datePublished,
modifiedTime: post.dateModified,
authors: [post.author],
images: [
{
url: post.image,
alt: post.imageAlt,
},
],
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.image],
},
};
}
export default function BlogPostPage({ params }: { params: { slug: string } }) {
// Prevent soft 404s for missing assets
if (params.slug.match(/\.(png|jpg|jpeg|gif|svg|ico|json|txt|xml)$/i)) {
notFound();
}
const post = blogPosts[params.slug];
if (!post) {
notFound();
}
const breadcrumbItems: BreadcrumbItem[] = [
{ name: 'Home', url: '/' },
{ name: 'Blog', url: '/blog' },
{ name: post.title, url: `/blog/${post.slug}` },
];
const schemas: any[] = [
blogPostingSchema({
title: post.title,
description: post.excerpt,
slug: post.slug,
author: post.author,
authorUrl: post.authorUrl,
datePublished: post.datePublished,
dateModified: post.dateModified,
image: post.image,
}),
breadcrumbSchema(breadcrumbItems),
];
if (post.howTo) {
schemas.push(howToSchema(post.howTo));
}
return (
<>
<SeoJsonLd data={schemas} />
<div className="py-20 bg-gradient-to-b from-gray-50 to-white">
<div className="container mx-auto px-4">
<div className="max-w-4xl mx-auto">
<Breadcrumbs items={breadcrumbItems} />
<article className="bg-white rounded-2xl shadow-sm p-8 md:p-12">
<header className="mb-10">
<div className="flex items-center flex-wrap gap-3 mb-6">
<Badge variant="info">{post.category}</Badge>
<span className="text-gray-500 flex items-center">
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{post.readTime} read
</span>
<span className="text-gray-500">By {post.author}</span>
<span className="text-gray-500">{post.date}</span>
</div>
<h1 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-6">
{post.title}
</h1>
{post.answer && (
<div className="bg-blue-50 border-l-4 border-blue-500 p-6 mb-8 rounded-r-lg">
<h2 className="text-xl font-semibold mb-2 text-gray-900">Quick Answer</h2>
<p className="text-lg text-gray-800 leading-relaxed">{post.answer}</p>
</div>
)}
<div className="relative w-full h-96 rounded-2xl overflow-hidden shadow-lg mb-8">
<Image
src={post.image}
alt={post.imageAlt}
fill
className="object-cover"
priority
/>
</div>
</header>
<div
className="prose prose-lg max-w-none
prose-headings:font-bold prose-headings:text-gray-900
prose-h2:text-3xl prose-h2:mt-12 prose-h2:mb-6
prose-h3:text-2xl prose-h3:mt-8 prose-h3:mb-4
prose-p:text-gray-700 prose-p:leading-relaxed prose-p:mb-6 prose-p:text-lg
prose-ul:my-6 prose-ul:space-y-2
prose-li:text-gray-700 prose-li:leading-relaxed
prose-strong:text-gray-900 prose-strong:font-semibold"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
{post.howTo && (
<div className="mt-12 bg-gradient-to-br from-blue-50 to-indigo-50 p-8 rounded-2xl border border-blue-200">
<h2 className="text-3xl font-bold text-gray-900 mb-6">{post.howTo.name}</h2>
<p className="text-lg text-gray-700 mb-6 leading-relaxed">{post.howTo.description}</p>
<ol className="space-y-6">
{post.howTo.steps.map((step: any, index: number) => (
<li key={index} className="flex items-start">
<span className="flex-shrink-0 w-10 h-10 bg-blue-600 text-white rounded-full flex items-center justify-center font-bold text-lg mr-4">
{index + 1}
</span>
<div className="flex-1">
<h3 className="font-semibold text-xl mb-2 text-gray-900">{step.name}</h3>
<p className="text-gray-700 leading-relaxed">{step.text}</p>
</div>
</li>
))}
</ol>
</div>
)}
<div className="mt-16 p-10 bg-gradient-to-br from-primary-50 to-primary-100 rounded-2xl text-center border border-primary-200">
<h2 className="text-3xl font-bold text-gray-900 mb-4">
Ready to Track Your QR Campaigns?
</h2>
<p className="text-lg text-gray-700 mb-8 max-w-2xl mx-auto leading-relaxed">
Start creating professional dynamic QR codes with advanced scan analytics, campaign tracking, and real-time insights.
</p>
<Link href="/signup">
<Button size="lg">Create QR Code Free</Button>
</Link>
</div>
{/* Related Articles Section */}
<div className="mt-16">
<h2 className="text-2xl font-bold text-gray-900 mb-8">Related Articles</h2>
<div className="overflow-x-auto pb-4 -mx-4 px-4">
<div className="flex gap-6" style={{ minWidth: 'max-content' }}>
{Object.values(blogPosts)
.filter((p) => p.slug !== post.slug)
.map((relatedPost) => (
<Link
key={relatedPost.slug}
href={`/blog/${relatedPost.slug}`}
className="group block bg-gray-50 rounded-xl p-6 hover:bg-gray-100 transition-colors flex-shrink-0"
style={{ width: '320px' }}
>
<Badge variant="default" className="mb-3">{relatedPost.category}</Badge>
<h3 className="font-semibold text-gray-900 group-hover:text-primary-600 transition-colors mb-2 line-clamp-2">
{relatedPost.title}
</h3>
<p className="text-sm text-gray-600 line-clamp-2">{relatedPost.excerpt}</p>
<span className="text-sm text-primary-600 mt-3 inline-block">Read more </span>
</Link>
))}
</div>
</div>
</div>
</article>
</div>
</div>
</div>
</>
);
}