8. Guided Project 1: A Personal Blog with CMS
This project will guide you through building a fully functional personal blog application using Next.js. We’ll integrate a headless CMS (Content Management System) to manage blog posts, demonstrating dynamic routing, data fetching in Server Components, and content rendering. This project will consolidate many of the concepts we’ve learned so far.
Project Objective: Create a personal blog where:
- Blog posts are managed by a headless CMS (we’ll simulate this with a local data source for simplicity, but the architecture is ready for a real CMS).
- Users can view a list of all blog posts.
- Users can click on a post to view its full content on a dedicated page.
- The blog is SEO-friendly with proper metadata.
Technology Stack:
- Next.js (App Router, Server Components)
- React
- Tailwind CSS (or your preferred styling method)
- (Simulated) Headless CMS for blog content
Step 1: Initialize Project Structure & Basic Layout
We already have a Next.js project. Let’s create the necessary routes and components for our blog.
1.1 Update Blog Data Source
We’ll use our existing src/lib/blogPosts.ts but extend it slightly to include more content, simulating a rich text editor from a CMS.
Action: Open src/lib/blogPosts.ts and update the posts array to include longer content fields.
// src/lib/blogPosts.ts
export interface BlogPost {
id: string;
title: string;
slug: string; // Add slug for cleaner URLs
content: string; // Longer content to simulate rich text
author: string;
date: string;
imageUrl?: string;
excerpt: string;
}
const posts: BlogPost[] = [
{
id: '1',
slug: 'getting-started-with-nextjs',
title: 'Getting Started with Next.js: Your First Steps',
content: `
<p class="mb-4">Welcome to the exciting world of Next.js! If you're eager to build modern, high-performance web applications with React, Next.js is an excellent choice. This post will guide you through the initial setup and core concepts.</p>
<h2 class="text-2xl font-semibold mb-3">What is Next.js?</h2>
<p class="mb-4">Next.js is a React framework that gives you the best developer experience with all the features you need for production: hybrid static & server rendering, TypeScript support, smart bundling, route pre-fetching, and more. Essentially, it takes care of many complex optimizations and configurations that you'd otherwise have to set up manually in a plain React application.</p>
<h2 class="text-2xl font-semibold mb-3">Why Next.js?</h2>
<ul class="list-disc list-inside mb-4">
<li><strong>Performance:</strong> Features like Server-Side Rendering (SSR) and Static Site Generation (SSG) provide blazing-fast page loads.</li>
<li><strong>SEO-Friendly:</strong> Pre-rendered content is easily crawlable by search engines, boosting your SEO.</li>
<li><strong>Developer Experience:</strong> Hot Module Replacement, file-system routing, and built-in optimizations streamline development.</li>
<li><strong>Full-Stack Capabilities:</strong> API Routes and Server Actions allow you to build full-stack applications within a single codebase.</li>
</ul>
<h2 class="text-2xl font-semibold mb-3">Your First Next.js Project</h2>
<p class="mb-4">To get started, make sure you have Node.js installed. Then, simply run:</p>
<pre class="bg-gray-800 text-white p-4 rounded-md overflow-x-auto font-mono text-left text-sm mb-4"><code>npx create-next-app@latest my-blog-app</code></pre>
<p class="mb-4">Follow the prompts, choosing the App Router and TypeScript. Once created, navigate into your project and run <code>npm run dev</code>. You'll see your first Next.js application running in your browser!</p>
<h2 class="text-2xl font-semibold mb-3">Next Steps</h2>
<p class="mb-4">Explore the `src/app` directory. The `page.tsx` file is your main page. Try making a small text change and see how Fast Refresh updates your browser instantly. In the next parts of this blog series, we'll dive deeper into routing, data fetching, and more advanced features.</p>
<p>Happy coding!</p>
`,
author: 'Jane Doe',
date: '2025-01-15',
imageUrl: 'https://images.unsplash.com/photo-1593720213428-fee66e40ce44?q=80&w=2938&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
excerpt: 'A beginner-friendly guide to Next.js fundamentals, covering initial setup, core features, and why it\'s a great choice for modern web development.'
},
{
id: '2',
slug: 'mastering-data-fetching-nextjs',
title: 'Mastering Data Fetching in Next.js: A Deep Dive',
content: `
<p class="mb-4">Efficient data fetching is at the heart of any dynamic web application. Next.js offers a spectrum of powerful strategies to retrieve data, ensuring optimal performance and user experience. Let's explore them.</p>
<h2 class="text-2xl font-semibold mb-3">Server Components: The New Default</h2>
<p class="mb-4">With the App Router, Server Components are the default. They run exclusively on the server, can directly access backend resources (like databases), and contribute zero JavaScript to the client bundle. This is ideal for initial page loads and content that doesn't require client-side interactivity.</p>
<pre class="bg-gray-800 text-white p-4 rounded-md overflow-x-auto font-mono text-left text-sm mb-4"><code>// app/blog/page.tsx (Server Component)
import { prisma } from '@/lib/prisma';
async function getPosts() {
return prisma.post.findMany();
}
export default async function BlogPosts() {
const posts = await getPosts();
// ... render posts
}</code></pre>
<h2 class="text-2xl font-semibold mb-3">Static Site Generation (SSG) with `generateStaticParams`</h2>
<p class="mb-4">For pages that can be pre-rendered at build time (e.g., static blogs), SSG is highly effective. Next.js fetches data once at build time and generates HTML files, which are then served from a CDN. `generateStaticParams` is key for dynamic routes with SSG.</p>
<pre class="bg-gray-800 text-white p-4 rounded-md overflow-x-auto font-mono text-left text-sm mb-4"><code>// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const slugs = await fetchSlugsFromAPI();
return slugs.map(slug => ({ slug }));
}
export default function BlogPostPage({ params }) {
// ... page content
}</code></pre>
<h2 class="text-2xl font-semibold mb-3">Server-Side Rendering (SSR)</h2>
<p class="mb-4">When data needs to be fresh on every request (e.g., personalized dashboards), SSR is your go-to. The page is rendered on the server for each request, ensuring the most up-to-date information. In Server Components, `fetch` with `cache: 'no-store'` or `next: { revalidate: 0 }` provides this behavior.</p>
<h2 class="text-2xl font-semibold mb-3">Client-Side Data Fetching</h2>
<p class="mb-4">For data that changes frequently after the initial page load, or data dependent on user interaction, client-side fetching is appropriate. This is done within Client Components using `useState` and `useEffect`, often with libraries like SWR or React Query.</p>
<pre class="bg-gray-800 text-white p-4 rounded-md overflow-x-auto font-mono text-left text-sm mb-4"><code>// components/MyInteractiveComponent.tsx
'use client';
import { useState, useEffect } from 'react';
function MyInteractiveComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/live-data').then(res => res.json()).then(setData);
}, []);
// ... render data
}</code></pre>
<h2 class="text-2xl font-semibold mb-3">Choosing the Right Strategy</h2>
<p class="mb-4">The key is to use the most "server-first" strategy possible for your content. Server Components for static/semi-static content, SSR for fresh data on every request, and client-side only when interactivity truly demands it. Next.js's caching and revalidation features further enhance these strategies.</p>
`,
author: 'John Smith',
date: '2025-03-20',
imageUrl: 'https://images.unsplash.com/photo-1549490104-5470783a32f0?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
excerpt: 'Understanding SSR, SSG, and client-side data loading. This article provides a comprehensive guide to data fetching techniques in Next.js, helping you build high-performance applications.'
},
{
id: '3',
slug: 'styling-nextjs-beautiful-uis',
title: 'Beautiful UIs: Styling in Next.js (Beyond the Basics)',
content: `
<p class="mb-4">Creating visually appealing and maintainable user interfaces is essential for any web application. Next.js provides a robust environment that supports various styling methodologies. Let's explore how to choose and effectively implement them for beautiful UIs.</p>
<h2 class="text-2xl font-semibold mb-3">Global CSS for Foundation</h2>
<p class="mb-4">Global CSS (e.g., <code>globals.css</code> imported in your root <code>layout.tsx</code>) is perfect for setting application-wide defaults: CSS resets, base typography, universal color palettes, and foundational layout elements like headers and footers. It ensures a consistent look and feel across your entire site.</p>
<pre class="bg-gray-800 text-white p-4 rounded-md overflow-x-auto font-mono text-left text-sm mb-4"><code>/* app/globals.css */
body {
font-family: var(--font-inter), sans-serif;
background-color: #f0f2f5;
color: #333;
}</code></pre>
<h2 class="text-2xl font-semibold mb-3">CSS Modules for Component Encapsulation</h2>
<p class="mb-4">CSS Modules allow you to locally scope CSS classes, preventing naming conflicts and promoting modularity. Files named <code>[name].module.css</code> are automatically processed, generating unique class names. This is excellent for self-contained UI components.</p>
<pre class="bg-gray-800 text-white p-4 rounded-md overflow-x-auto font-mono text-left text-sm mb-4"><code>/* components/Button.module.css */
.button {
background-color: #0070f3;
color: white;
padding: 10px 20px;
border-radius: 5px;
}</code></pre>
<h2 class="text-2xl font-semibold mb-3">Tailwind CSS: Utility-First Powerhouse</h2>
<p class="mb-4">Tailwind CSS is a utility-first framework that lets you rapidly build custom designs directly in your JSX markup. Instead of writing custom CSS, you apply pre-defined utility classes (e.g., <code>flex</code>, <code>p-4</code>, <code>text-center</code>). It's highly configurable and excellent for responsive design and quick iterations.</p>
<pre class="bg-gray-800 text-white p-4 rounded-md overflow-x-auto font-mono text-left text-sm mb-4"><code>{`<button className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded">
Click Me
</button>`}</code></pre>
<h2 class="text-2xl font-semibold mb-3">Choosing Your Strategy</h2>
<p class="mb-4">The best approach often involves a combination:</p>
<ul class="list-disc list-inside mb-4">
<li>**Global CSS:** For universal defaults and third-party libraries.</li>
<li>**Tailwind CSS:** For most component styling, layout, and responsiveness due to its speed and comprehensive utilities.</li>
<li>**CSS Modules:** For specific, complex components where you need traditional CSS power or have specific scoping requirements.</li>
</ul>
<p class="mb-4">Next.js also integrates seamlessly with `next/image` for optimized images and `next/font` for self-hosted, optimized fonts, ensuring your styled components are delivered performantly.</p>
<p>By understanding and strategically applying these methods, you can create stunning UIs that are both beautiful and performant.</p>
`,
author: 'Alice Wonderland',
date: '2025-05-10',
imageUrl: 'https://images.unsplash.com/photo-1581472723648-5264b3c4f74d?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
excerpt: 'A guide to CSS Modules, Tailwind, and more. This article explores the best practices for styling your Next.js applications to achieve visually appealing and easily maintainable user interfaces.'
}
];
export async function getPostDetails(slug: string): Promise<BlogPost | undefined> { // Use slug for lookup
await new Promise(resolve => setTimeout(resolve, 500));
return posts.find(post => post.slug === slug);
}
export async function getAllPostSlugs(): Promise<string[]> { // Get slugs for static params
await new Promise(resolve => setTimeout(resolve, 100));
return posts.map(post => post.slug);
}
export async function getAllBlogPosts(): Promise<BlogPost[]> {
await new Promise(resolve => setTimeout(resolve, 100));
return posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); // Sort by date
}
1.2 Update Blog Listing Page
Our src/app/blog/page.tsx needs to display the excerpt and link using the new slug.
Action: Open src/app/blog/page.tsx and update it.
// src/app/blog/page.tsx
import Link from 'next/link';
import { getAllBlogPosts } from '@/lib/blogPosts';
export const metadata = {
title: 'Blog | My Next.js App',
description: 'Read the latest articles and tutorials on web development with Next.js.',
};
export default async function BlogPage() {
const posts = await getAllBlogPosts();
return (
<main className="container mx-auto p-6 max-w-3xl font-sans text-gray-800">
<h1 className="text-4xl font-extrabold text-gray-900 mb-6 text-center">Latest Blog Posts</h1>
<p className="text-lg text-gray-700 mb-8 text-center">Stay updated with our latest articles and tutorials on Next.js and web development.</p>
<ul className="list-none p-0">
{posts.map((post) => (
<li key={post.id} className="mb-6 p-6 bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200">
{/* Link to the dynamic post page using the slug */}
<Link href={`/blog/${post.slug}`} className="block text-decoration-none">
<h2 className="text-3xl font-bold text-blue-700 hover:text-blue-900 mb-2">
{post.title}
</h2>
<p className="text-gray-600 text-sm mb-2">
By {post.author} on {new Date(post.date).toLocaleDateString()}
</p>
<p className="text-gray-600 mb-4">{post.excerpt}</p>
<span className="text-blue-600 font-medium hover:underline">Read More →</span>
</Link>
</li>
))}
</ul>
<div className="mt-8 text-center">
<Link href="/" className="text-gray-600 hover:text-gray-800 font-medium text-lg">← Back to Home</Link>
</div>
</main>
);
}
1.3 Update Dynamic Blog Post Page
Our src/app/blog/[postId]/page.tsx now needs to use the slug from params and dangerouslySetInnerHTML to render the rich text content. Also, we will update generateStaticParams to use slugs.
Action: Open src/app/blog/[postId]/page.tsx and update it. Rename the folder [postId] to [slug] for clarity if you wish, but the code assumes [postId] for now.
// src/app/blog/[postId]/page.tsx
import type { Metadata, ResolvingMetadata } from 'next';
import { getPostDetails, getAllPostSlugs } from '@/lib/blogPosts'; // Use getAllPostSlugs
import { notFound } from 'next/navigation';
import Link from 'next/link';
import Image from 'next/image';
interface BlogPostPageProps {
params: { postId: string }; // Keep postId for consistency, but internally treat as slug
}
// --- Dynamic Metadata Generation ---
export async function generateMetadata(
{ params }: BlogPostPageProps,
parent: ResolvingMetadata
): Promise<Metadata> {
const post = await getPostDetails(params.postId); // Use postId as slug for lookup
if (!post) {
return {
title: 'Post Not Found',
description: 'The requested blog post could not be found.',
};
}
const previousImages = (await parent).openGraph?.images || [];
return {
title: post.title,
description: post.excerpt,
keywords: [post.title.split(' ')[0], 'Next.js', 'blog', post.author],
authors: [{ name: post.author }],
openGraph: {
title: post.title,
description: post.excerpt,
url: `https://yourwebsite.com/blog/${post.slug}`, // Use slug for canonical URL
siteName: 'My Next.js App',
images: post.imageUrl ? [{ url: post.imageUrl, width: 1200, height: 630, alt: post.title }] : previousImages,
locale: 'en_US',
type: 'article',
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: post.imageUrl ? [post.imageUrl] : previousImages,
creator: `@${post.author.replace(/\s/g, '')}`,
},
};
}
// --- Dynamic Page Content ---
export default async function BlogPostPage({ params }: BlogPostPageProps) {
const post = await getPostDetails(params.postId); // Use postId as slug for lookup
if (!post) {
notFound();
}
return (
<main className="container mx-auto p-6 max-w-3xl bg-white shadow-lg rounded-lg mt-10 mb-10">
<Link href="/blog" className="text-blue-600 hover:text-blue-800 mb-4 inline-block">← Back to Blog</Link>
{post.imageUrl && (
<Image
src={post.imageUrl}
alt={post.title}
width={800}
height={450}
priority
className="rounded-md w-full h-auto object-cover mb-6"
/>
)}
<h1 className="text-4xl font-extrabold text-gray-900 mb-4">{post.title}</h1>
<p className="text-gray-600 text-sm mb-4">
By {post.author} on {new Date(post.date).toLocaleDateString()}
</p>
<div
className="prose prose-lg max-w-none text-gray-700"
dangerouslySetInnerHTML={{ __html: post.content }} // Render rich text content
/>
</main>
);
}
// Optional: generateStaticParams for SSG and dynamic routes
export async function generateStaticParams() {
const postSlugs = await getAllPostSlugs(); // Get slugs
return postSlugs.map((slug) => ({
postId: slug, // Map slug to postId parameter
}));
}
1.4 Update next.config.mjs for Images (if you haven’t already)
Ensure images.remotePatterns includes unsplash.com.
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
port: '',
pathname: '**',
},
// ...any other remote patterns
],
},
};
export default nextConfig;
Action: Save all modified files.
Test:
- Run
npm run dev. - Navigate to
http://localhost:3000/blog. - Click on any blog post title. You should see the detailed post with rich content and an image.
- Observe the URL, it uses the
slug. - Check the browser’s head section in dev tools to confirm metadata is set correctly.
Step 2: Implement a Simple Comment Section (Client Component)
Most blogs have a comment section. We’ll build a basic one using a Client Component to demonstrate interactivity.
2.1 Create Comment Form Client Component
Action: Create a new folder src/app/blog/[postId]/components and a file src/app/blog/[postId]/components/CommentForm.tsx.
mkdir src/app/blog/[postId]/components
touch src/app/blog/[postId]/components/CommentForm.tsx
// src/app/blog/[postId]/components/CommentForm.tsx
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useSession } from 'next-auth/react'; // For checking if user is logged in
interface CommentFormProps {
postId: string;
}
interface Comment {
id: string;
author: string;
text: string;
date: string;
}
// Simulated comments for now; in a real app, this would be fetched from DB
const mockComments: Comment[] = [];
export default function CommentForm({ postId }: CommentFormProps) {
const { data: session } = useSession();
const router = useRouter();
const [commentText, setCommentText] = useState('');
const [comments, setComments] = useState<Comment[]>(mockComments); // Initialize with mock comments
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!commentText.trim()) return;
if (!session) {
alert('You need to be logged in to post a comment.');
router.push(`/auth/login?callbackUrl=/blog/${postId}`);
return;
}
setLoading(true);
setError(null);
try {
// Simulate API call to post comment
await new Promise(resolve => setTimeout(resolve, 800));
const newComment: Comment = {
id: Date.now().toString(), // Simple unique ID
author: session.user?.name || session.user?.email || 'Anonymous',
text: commentText.trim(),
date: new Date().toISOString(),
};
setComments(prev => [...prev, newComment]); // Add to local state for instant display
setCommentText(''); // Clear input
// In a real app, you'd send this to an API route or Server Action
console.log('Comment submitted:', newComment);
} catch (err) {
setError('Failed to post comment.');
console.error(err);
} finally {
setLoading(false);
}
};
return (
<div className="mt-8 border-t border-dashed border-gray-300 pt-6">
<h3 className="text-2xl font-bold text-gray-900 mb-4">Comments</h3>
{comments.length === 0 ? (
<p className="text-gray-600 mb-4">No comments yet. Be the first to comment!</p>
) : (
<ul className="mb-6 space-y-4">
{comments.map((comment) => (
<li key={comment.id} className="bg-gray-50 p-4 rounded-md shadow-sm">
<p className="font-semibold text-gray-800">{comment.author}</p>
<p className="text-sm text-gray-500 mb-2">{new Date(comment.date).toLocaleDateString()} {new Date(comment.date).toLocaleTimeString()}</p>
<p className="text-gray-700">{comment.text}</p>
</li>
))}
</ul>
)}
{session ? (
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<textarea
className="w-full p-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={4}
placeholder="Write your comment here..."
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
disabled={loading}
required
></textarea>
{error && <p className="text-red-500 text-sm">{error}</p>}
<button
type="submit"
className="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-md disabled:opacity-50 disabled:cursor-not-allowed self-end"
disabled={loading}
>
{loading ? 'Posting...' : 'Post Comment'}
</button>
</form>
) : (
<p className="text-gray-600 text-center py-4">
<Link href={`/auth/login?callbackUrl=/blog/${postId}`} className="text-blue-600 hover:underline font-medium">
Log in
</Link> to post a comment.
</p>
)}
</div>
);
}
2.2 Integrate Comment Form into Blog Post Page
Action: Open src/app/blog/[postId]/page.tsx and add the CommentForm at the bottom of the page.
// src/app/blog/[postId]/page.tsx
// ... (existing imports)
import CommentForm from './components/CommentForm'; // Import the CommentForm
// ... (generateMetadata and BlogPostPageProps)
export default async function BlogPostPage({ params }: BlogPostPageProps) {
const post = await getPostDetails(params.postId);
if (!post) {
notFound();
}
return (
<main className="container mx-auto p-6 max-w-3xl bg-white shadow-lg rounded-lg mt-10 mb-10">
<Link href="/blog" className="text-blue-600 hover:text-blue-800 mb-4 inline-block">← Back to Blog</Link>
{post.imageUrl && (
<Image
src={post.imageUrl}
alt={post.title}
width={800}
height={450}
priority
className="rounded-md w-full h-auto object-cover mb-6"
/>
)}
<h1 className="text-4xl font-extrabold text-gray-900 mb-4">{post.title}</h1>
<p className="text-gray-600 text-sm mb-4">
By {post.author} on {new Date(post.date).toLocaleDateString()}
</p>
<div
className="prose prose-lg max-w-none text-gray-700"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
{/* Integrate the CommentForm */}
<CommentForm postId={post.slug} /> {/* Pass the post slug as postId */}
</main>
);
}
// ... (generateStaticParams)
Test:
- Ensure your
npm run devserver is running. - Log in to your application (
http://localhost:3000/auth/login). - Navigate to a blog post (e.g.,
http://localhost:3000/blog/getting-started-with-nextjs). - Scroll down to the comment section. If logged in, you should see the comment form.
- Try submitting a comment. It should appear immediately below.
- Log out and try to access the comment section again. You should see the “Log in to post a comment” message.
Step 3: Implement Recent Posts Sidebar (Advanced Component with Server-Client Mix)
Let’s add a “Recent Posts” sidebar to our main blog page (/blog) that fetches data and displays it. This will be a Server Component to efficiently fetch the data, potentially including an interactive “Load More” button (Client Component).
3.1 Create Recent Posts Server Component
Action: Create src/app/blog/components/RecentPostsSidebar.tsx.
touch src/app/blog/components/RecentPostsSidebar.tsx
// src/app/blog/components/RecentPostsSidebar.tsx
import { getAllBlogPosts } from '@/lib/blogPosts';
import Link from 'next/link';
import Image from 'next/image';
import { Suspense } from 'react';
// Optional: A loading state for the sidebar if it takes a while to load
function RecentPostsLoading() {
return (
<div className="bg-white p-6 rounded-lg shadow-md animate-pulse">
<h3 className="text-xl font-bold mb-4 text-gray-900">Recent Posts</h3>
<div className="space-y-4">
<div className="h-6 bg-gray-200 rounded w-3/4 mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
<div className="h-6 bg-gray-200 rounded w-3/4 mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
<div className="h-6 bg-gray-200 rounded w-3/4 mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
</div>
</div>
);
}
// Server Component for fetching and displaying recent posts
export default async function RecentPostsSidebar() {
// Simulate a slightly longer delay for the sidebar content
await new Promise(resolve => setTimeout(resolve, 700));
const allPosts = await getAllBlogPosts();
const recentPosts = allPosts.slice(0, 5); // Get the 5 most recent posts
return (
<div className="bg-white p-6 rounded-lg shadow-md sticky top-6">
<h3 className="text-xl font-bold text-gray-900 mb-4">Recent Posts</h3>
<ul className="space-y-4">
{recentPosts.map((post) => (
<li key={post.id} className="flex items-start gap-3">
{post.imageUrl && (
<Link href={`/blog/${post.slug}`} className="flex-shrink-0">
<Image
src={post.imageUrl}
alt={post.title}
width={80}
height={50}
className="rounded-md object-cover"
/>
</Link>
)}
<div>
<Link href={`/blog/${post.slug}`} className="font-semibold text-blue-700 hover:text-blue-900 line-clamp-2">
{post.title}
</Link>
<p className="text-xs text-gray-500">{new Date(post.date).toLocaleDateString()}</p>
</div>
</li>
))}
</ul>
{/* You could add a "Load More" button here (as a Client Component) */}
<div className="mt-6 text-center">
<Link href="/blog" className="text-blue-600 hover:underline font-medium">
View All Posts
</Link>
</div>
</div>
);
}
3.2 Integrate Sidebar into Blog Layout
To make the sidebar appear next to our main blog content, we’ll modify the src/app/blog/page.tsx structure to use a flex layout.
Action: Open src/app/blog/page.tsx and modify its return structure.
// src/app/blog/page.tsx
import Link from 'next/link';
import { getAllBlogPosts } from '@/lib/blogPosts';
import RecentPostsSidebar from './components/RecentPostsSidebar'; // Import the sidebar
import { Suspense } from 'react'; // Import Suspense for loading states
export const metadata = {
title: 'Blog | My Next.js App',
description: 'Read the latest articles and tutorials on web development with Next.js.',
};
export default async function BlogPage() {
const posts = await getAllBlogPosts();
return (
<div className="container mx-auto p-6 max-w-5xl font-sans text-gray-800 flex flex-col md:flex-row gap-8">
{/* Main Content Area */}
<main className="flex-grow">
<h1 className="text-4xl font-extrabold text-gray-900 mb-6 text-center md:text-left">Latest Blog Posts</h1>
<p className="text-lg text-gray-700 mb-8 text-center md:text-left">Stay updated with our latest articles and tutorials on Next.js and web development.</p>
<ul className="list-none p-0">
{posts.map((post) => (
<li key={post.id} className="mb-6 p-6 bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200">
<Link href={`/blog/${post.slug}`} className="block text-decoration-none">
<h2 className="text-3xl font-bold text-blue-700 hover:text-blue-900 mb-2">
{post.title}
</h2>
<p className="text-gray-600 text-sm mb-2">
By {post.author} on {new Date(post.date).toLocaleDateString()}
</p>
<p className="text-gray-600 mb-4">{post.excerpt}</p>
<span className="text-blue-600 font-medium hover:underline">Read More →</span>
</Link>
</li>
))}
</ul>
<div className="mt-8 text-center">
<Link href="/" className="text-gray-600 hover:text-gray-800 font-medium text-lg">← Back to Home</Link>
</div>
</main>
{/* Sidebar Area */}
<aside className="w-full md:w-1/3 lg:w-1/4">
{/* Wrap sidebar with Suspense to show loading state */}
<Suspense fallback={<RecentPostsLoading />}>
<RecentPostsSidebar />
</Suspense>
</aside>
</div>
);
}
Test:
- Ensure your
npm run devserver is running. - Navigate to
http://localhost:3000/blog. - You should now see the list of blog posts on the left and a “Recent Posts” sidebar on the right.
- Observe the loading state for the sidebar if its data fetch takes longer.
Step 4: Add Social Share Buttons (Client Component)
Finally, let’s add some social share buttons to our blog post page to allow users to easily share content. These will be Client Components as they interact with browser APIs.
4.1 Create Social Share Buttons Component
Action: Create src/app/blog/[postId]/components/SocialShareButtons.tsx.
touch src/app/blog/[postId]/components/SocialShareButtons.tsx
// src/app/blog/[postId]/components/SocialShareButtons.tsx
'use client';
import { FaFacebook, FaTwitter, FaLinkedin, FaWhatsapp } from 'react-icons/fa'; // You'll need to install react-icons
interface SocialShareButtonsProps {
title: string;
url: string;
}
export default function SocialShareButtons({ title, url }: SocialShareButtonsProps) {
const shareText = `Check out this amazing blog post: ${title}`;
const openShareWindow = (shareUrl: string) => {
window.open(shareUrl, '_blank', 'noopener,noreferrer,width=600,height=400');
};
return (
<div className="mt-8 pt-6 border-t border-dashed border-gray-300 flex items-center gap-4 justify-center">
<span className="text-lg font-semibold text-gray-800">Share this post:</span>
<button
onClick={() => openShareWindow(`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`)}
className="text-blue-700 hover:text-blue-900 text-3xl"
aria-label="Share on Facebook"
>
<FaFacebook />
</button>
<button
onClick={() => openShareWindow(`https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(shareText)}`)}
className="text-blue-400 hover:text-blue-600 text-3xl"
aria-label="Share on Twitter"
>
<FaTwitter />
</button>
<button
onClick={() => openShareWindow(`https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(url)}&title=${encodeURIComponent(title)}&summary=${encodeURIComponent(shareText)}&source=${encodeURIComponent(url)}`)}
className="text-blue-600 hover:text-blue-800 text-3xl"
aria-label="Share on LinkedIn"
>
<FaLinkedin />
</button>
<button
onClick={() => openShareWindow(`https://wa.me/?text=${encodeURIComponent(shareText + ' ' + url)}`)}
className="text-green-500 hover:text-green-700 text-3xl"
aria-label="Share on WhatsApp"
>
<FaWhatsapp />
</button>
</div>
);
}
Action: Install react-icons:
npm install react-icons
4.2 Integrate Share Buttons into Blog Post Page
Action: Open src/app/blog/[postId]/page.tsx and add SocialShareButtons at the bottom.
// src/app/blog/[postId]/page.tsx
// ... (existing imports)
import SocialShareButtons from './components/SocialShareButtons'; // Import the share buttons
// ... (generateMetadata and BlogPostPageProps)
export default async function BlogPostPage({ params }: BlogPostPageProps) {
const post = await getPostDetails(params.postId);
if (!post) {
notFound();
}
// Get the full URL for sharing (mock for now, use actual domain in production)
const fullUrl = `http://localhost:3000/blog/${post.slug}`;
return (
<main className="container mx-auto p-6 max-w-3xl bg-white shadow-lg rounded-lg mt-10 mb-10">
<Link href="/blog" className="text-blue-600 hover:text-blue-800 mb-4 inline-block">← Back to Blog</Link>
{post.imageUrl && (
<Image
src={post.imageUrl}
alt={post.title}
width={800}
height={450}
priority
className="rounded-md w-full h-auto object-cover mb-6"
/>
)}
<h1 className="text-4xl font-extrabold text-gray-900 mb-4">{post.title}</h1>
<p className="text-gray-600 text-sm mb-4">
By {post.author} on {new Date(post.date).toLocaleDateString()}
</p>
<div
className="prose prose-lg max-w-none text-gray-700"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
{/* Integrate the SocialShareButtons */}
<SocialShareButtons title={post.title} url={fullUrl} />
{/* Integrate the CommentForm */}
<CommentForm postId={post.slug} />
</main>
);
}
// ... (generateStaticParams)
Test:
- Ensure
npm run devis running. - Navigate to a blog post.
- Scroll down, and you should see the social share buttons.
- Click them to open sharing pop-ups (note: sharing functionality depends on your browser and being logged into social media).
Project Summary:
Congratulations! You’ve successfully built a functional blog application in Next.js, demonstrating:
- Dynamic routing with slugs.
- Server-side data fetching for initial content.
- Rendering rich HTML content (
dangerouslySetInnerHTML). - Client-side interactivity (comment form, social sharing).
- Authentication integration for protected features.
- Metadata generation for SEO.
- A responsive layout with a sidebar.
This project provided a hands-on experience in combining various Next.js features to create a robust and dynamic web application. Remember that in a real-world scenario, the blog post data and comments would come from a real database and a headless CMS like Contentful, Strapi, or Sanity.io.