Next-Blog-AI
Home/Documentation/Examples & Resources/Examples & Recipes
Back to Documentation

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