Examples & Recipes
Practical examples to help you integrate Next-Blog-AI in your applications
This guide provides practical examples based on real implementation patterns. You can use these examples as starting points for integrating Next-Blog-AI in your Next.js applications.
Blog List Page (App Router)
Create a blog listing page with proper metadata and format switching:
// app/blog/page.tsx
import type { Metadata } from 'next';
import { Suspense } from 'react';
import { nextBlogAI } from '@/lib/blog-api';
// Dynamic metadata for the blog list page
export async function generateMetadata(): Promise<Metadata> {
try {
// Fetch only SEO data with optimized endpoint
const { data, error } = await nextBlogAI.getBlogListSEO({
next: { revalidate: 3600 }, // Cache for 1 hour
});
if (error || !data) return {
title: 'Blog',
description: 'Blog posts and articles'
};
// Return dynamic SEO metadata
return {
title: data.seo.title,
description: data.seo.description,
keywords: data.seo.keywords.join(', '),
};
} catch (error) {
console.error('Error generating blog list metadata:', error);
return {
title: 'Blog',
description: 'Blog posts and articles'
};
}
}
// Server component to fetch and render blog posts
async function BlogList({
format = 'html',
page = 1
}: {
format?: 'html' | 'json';
page?: number;
}) {
// Fetch blog posts with the requested format and page
const { data, error } = await nextBlogAI.getBlogPosts({
format,
page,
perPage: 9,
next: { revalidate: 3600 }, // Cache for 1 hour
});
if (error) {
return <div>Error loading posts: {error.message}</div>;
}
// If we have HTML content, render it directly
if (data?.format === 'html') {
return <div dangerouslySetInnerHTML={{ __html: data.content }} />;
}
// For JSON format, create a custom UI
if (data?.format === 'json') {
return (
<div>
<h1 className='text-3xl font-bold mb-6'>Blog Posts</h1>
<div className='grid gap-6 md:grid-cols-2 lg:grid-cols-3'>
{data.posts.map(post => (
<article key={post.id} className='border rounded-lg p-4'>
<h2 className='text-xl font-bold mb-2'>{post.title}</h2>
<p className='text-gray-600 mb-3'>{post.excerpt}</p>
<a href={`/blog/${post.slug}`} className='text-blue-600 hover:underline'>
Read more
</a>
</article>
))}
</div>
</div>
);
}
return <div>No blog posts found.</div>;
}
export default async function BlogPage({
searchParams
}: {
searchParams: Promise<{ format?: 'html' | 'json'; page?: string }>;
}) {
// Get query parameters with defaults
const { format, page } = await searchParams;
const currentPage = page ? parseInt(page) : 1;
const currentFormat = format === 'json' ? 'json' : 'html';
return (
<div className='container mx-auto px-4 py-8'>
<Suspense fallback={<div>Loading blog posts...</div>}>
<BlogList format={currentFormat} page={currentPage} />
</Suspense>
</div>
);
}
This example demonstrates a complete blog listing page with:
• Dynamic SEO metadata generation
• Format switching (HTML/JSON)
• Pagination support
• Error handling and suspense
Blog Post Detail Page
Create a dynamic route for individual blog posts with proper SEO:
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';
import { Suspense } from 'react';
import { nextBlogAI } from '@/lib/blog-api';
type Props = {
params: Promise<{ slug: string }>;
searchParams: Promise<{ format?: 'html' | 'json' }>;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
try {
const { slug } = await params;
// Use the optimized SEO-only function
const { data, error } = await nextBlogAI.getBlogPostSEO(slug, {
next: { revalidate: 3600 }, // Cache for 1 hour
});
if (error || !data) {
return {
title: 'Blog Post',
description: 'Read our latest article'
};
}
// Return rich metadata including OpenGraph
return {
title: data.seo.title,
description: data.seo.description,
keywords: data.seo.keywords.join(', '),
openGraph: {
title: data.seo.title,
description: data.seo.description,
type: 'article',
// Add image if available
...(data.seo.featuredImage && {
images: [{
url: data.seo.featuredImage.url,
width: 1200,
height: 630,
alt: data.seo.featuredImage.alt
}]
})
}
};
} catch (error) {
console.error('Error generating metadata:', error);
return {
title: 'Blog Post',
description: 'Read our latest article'
};
}
}
// Server component to fetch and render a blog post
async function BlogPost({
slug,
format = 'html'
}: {
slug: string;
format: 'html' | 'json';
}) {
try {
// Fetch the blog post
const { data, error } = await nextBlogAI.getBlogPost(slug, {
format,
next: { revalidate: 3600 }, // Cache for 1 hour
});
if (error) {
return <div>Error loading post: {error.message}</div>;
}
if (!data) {
return <div>Blog post not found</div>;
}
// For HTML format, render the pre-styled content
if (data.format === 'html') {
return <div dangerouslySetInnerHTML={{ __html: data.content }} />;
}
// For JSON format, create a custom UI
if (data.format === 'json') {
return (
<article>
<h1 className='text-3xl font-bold mb-4'>{data.post.title}</h1>
<div className='text-gray-500 mb-6'>
{new Date(data.post.publishedAt).toLocaleDateString()} •
{data.post.readingTime} min read
</div>
<div className='prose max-w-none'>{data.post.content}</div>
{/* Comments section */}
<h2 className='text-2xl font-bold mt-8 mb-4'>Comments</h2>
{data.comments.length > 0 ? (
<div className='space-y-4'>
{data.comments.map(comment => (
<div key={comment.id} className='border-b pb-4'>
<div className='font-bold'>{comment.authorName}</div>
<div className='text-gray-500 text-sm mb-2'>
{new Date(comment.createdAt).toLocaleDateString()}
</div>
<p>{comment.content}</p>
</div>
))}
</div>
) : (
<p>No comments yet. Be the first to comment!</p>
)}
</article>
);
}
} catch (error) {
console.error('Error rendering blog post:', error);
return <div>Failed to load blog post. Please try again later.</div>;
}
}
export default async function BlogPostPage({ params, searchParams }: Props) {
const { slug } = await params;
const { format } = await searchParams;
const currentFormat = format === 'json' ? 'json' : 'html';
return (
<div className='container mx-auto px-4 py-8 max-w-3xl'>
<Suspense fallback={<div>Loading blog post...</div>}>
<BlogPost slug={slug} format={currentFormat} />
</Suspense>
</div>
);
}
This example shows how to create a dynamic blog post page with:
• Rich SEO metadata including OpenGraph for social sharing
• HTML or JSON format rendering
• Comments display
• Error handling and suspense
Next.js Comment Form Component
Add a comment form to your Next.js blog posts using the App Router:
// app/blog/[slug]/components/CommentForm.tsx
'use client';
import { useState } from 'react';
import { toast } from 'sonner'; // Or your preferred toast library
import { nextBlogAI } from '@/lib/next-blog-ai';
// Note: This approach is specific to JSON format usage in Next.js.
// When using HTML format, the comment form is automatically included in the response
// and you don't need to implement it manually.
interface CommentFormProps {
postId: string;
onSuccess?: () => void;
}
export default function CommentForm({ postId, onSuccess }: CommentFormProps) {
const [form, setForm] = useState({
authorName: '',
authorEmail: '',
content: ''
});
const [loading, setLoading] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setForm(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
// Use the submitComment function from the next-blog-ai package
const { data, error } = await nextBlogAI.submitComment({
postId: postId,
authorName: form.authorName,
authorEmail: form.authorEmail,
content: form.content
});
if (error) {
throw new Error(error.message || 'Failed to submit comment');
}
// Reset form
setForm({ authorName: '', authorEmail: '', content: '' });
// Show success message
toast.success('Comment submitted successfully!');
// Call onSuccess callback if provided
if (onSuccess) onSuccess();
} catch (error) {
console.error('Error submitting comment:', error);
toast.error(error.message || 'Failed to submit comment');
} finally {
setLoading(false);
}
};
return (
<div className="mt-8 border-t pt-6">
<h3 className="text-xl font-bold mb-4">Leave a Comment</h3>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="authorName" className="block text-sm font-medium mb-1">
Name *
</label>
<input
id="authorName"
name="authorName"
type="text"
value={form.authorName}
onChange={handleChange}
required
className="w-full px-3 py-2 border rounded-md"
disabled={loading}
/>
</div>
<div>
<label htmlFor="authorEmail" className="block text-sm font-medium mb-1">
Email *
</label>
<input
id="authorEmail"
name="authorEmail"
type="email"
value={form.authorEmail}
onChange={handleChange}
required
className="w-full px-3 py-2 border rounded-md"
disabled={loading}
/>
</div>
<div>
<label htmlFor="content" className="block text-sm font-medium mb-1">
Comment *
</label>
<textarea
id="content"
name="content"
value={form.content}
onChange={handleChange}
required
rows={4}
className="w-full px-3 py-2 border rounded-md"
disabled={loading}
/>
</div>
<button
type="submit"
disabled={loading}
className="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 disabled:opacity-50"
>
{loading ? 'Submitting...' : 'Submit Comment'}
</button>
</form>
</div>
);
}
This Next.js client component provides a complete comment form with:
• Integration with Next.js App Router
• Client-side form validation
• Loading states and error handling
• Uses the next-blog-ai package directly
• Shows how to set up a shared client instance for use across components
Recent Blog Posts Component
Display recent blog posts in any part of your site:
// components/blog/RecentPosts.tsx
import { nextBlogAI } from '@/lib/blog-api';
import Link from 'next/link';
export default async function RecentPosts({ limit = 3 }: { limit?: number }) {
try {
// Fetch recent posts with only the essential fields
const { data, error } = await nextBlogAI.getBlogPosts({
perPage: limit,
format: 'json',
next: { revalidate: 3600 } // Cache for 1 hour
});
if (error || !data || data.format !== 'json') {
// Handle error gracefully with an empty state
return (
<div className="py-2">
<Link href="/blog" className="text-primary hover:underline">
View all blog posts →
</Link>
</div>
);
}
// Get the most recent posts
const recentPosts = data.posts.slice(0, limit);
return (
<div className="space-y-4">
<h3 className="font-bold text-lg">Recent Articles</h3>
<ul className="space-y-2">
{recentPosts.map(post => (
<li key={post.id}>
<Link
href={`/blog/${post.slug}`}
className='text-sm text-muted-foreground hover:text-foreground hover:underline block truncate'
title={post.title}
>
{post.title}
</Link>
</li>
))}
</ul>
<div className="pt-2">
<Link href="/blog" className="text-primary hover:underline text-sm">
View all blog posts →
</Link>
</div>
</div>
);
} catch (error) {
console.error('Error fetching recent posts:', error);
return null;
}
}
This server component provides a clean way to:
• Display recent blog posts anywhere in your site
• Show a truncated list with "View all" link
• Handle errors gracefully
• Cache data for optimal performance