≡ Menu

🛠️ Building a Server-Powered Blog with Next.js Server Components

React Server Components (RSC) have redefined modern web development, offering massive performance gains by shifting rendering and data fetching from the browser to the server. If you’re using the Next.js App Router, you’re already using RSC!

This post walks you through building a simple, highly performant blog using Next.js Server Components for data fetching and rendering, coupled with Client Components only where true interactivity is needed.


The Power Duo: Server Components (Default) and Client Components ('use client')

In Next.js App Router, every component is a Server Component by default. This is the key to performance, as these components:

  1. Can fetch data directly (using async/await).

  2. Have access to server-side resources (like environment variables).

  3. Do not ship their code to the client’s JavaScript bundle.

We use Client Components only for features requiring browser APIs, state, or event handlers (like onClick).

1. 📂 Mocking Server-Side Data (lib/posts.ts)

In a real-world scenario, the function below would contain your database query (e.g., using Prisma or an ORM). Here, we simulate that server-side access and a network delay.

// src/lib/posts.ts

export interface Post {
  id: string;
  title: string;
  content: string;
  author: string;
  date: string;
}

export async function getPosts(): Promise<Post[]> {
  // Simulate network delay and database query
  await new Promise((resolve) => setTimeout(resolve, 1000));

  return [
    /* ... mock data objects ... */
    // { id: '1', title: '...', content: '...', author: '...' },
    // { id: '2', title: '...', content: '...', author: '...' },
  ];
}

By placing this code on the server, we ensure that secrets or connection strings used within getPosts are never exposed to the client.

2. ❤️ The Interactive Client Component (components/LikeButton.tsx)

This component handles user interaction (the button click) and manages local state (the like count). Because it needs browser capabilities, we mark it with the 'use client' directive.

// src/components/LikeButton.tsx
'use client'; 

import { useState } from 'react';

// ... component definition ...

export default function LikeButton({ initialLikes }: LikeButtonProps) {
  const [likes, setLikes] = useState(initialLikes);

  const handleClick = () => {
    setLikes((prevLikes) => prevLikes + 1);
    // Real-world: Send update to a Server Action or API endpoint
  };

  return (
    <button
      onClick={handleClick}
      // ... Tailwind CSS classes ...
    >
      ❤️ Like ({likes})
    </button>
  );
}

Key Takeaway: The Client Component file is the only one whose code is sent to the browser.

3. 📝 The Rendering Server Component (components/BlogPost.tsx)

This component receives the post data (fetched on the server) and structures the UI. It remains a Server Component, even though it renders the LikeButton Client Component inside it.

// src/components/BlogPost.tsx

import { Post } from '@/lib/posts';
import LikeButton from './LikeButton'; // Importing a Client Component

// ... component definition ...

export default function BlogPost({ post }: BlogPostProps) {
  return (
    <article className="bg-white p-6 rounded-lg shadow-md mb-8">
      <h2 className="text-3xl font-bold text-gray-800 mb-2">{post.title}</h2>
      {/* ... other post details ... */}
      <div className="prose prose-lg text-gray-700">
        <p>{post.content}</p>
      </div>
      
      {/* Renders the static HTML of the LikeButton, then hydrates it */}
      <LikeButton initialLikes={Math.floor(Math.random() * 100)} /> 
    </article>
  );
}

4. 🌐 The Main Page Component (app/page.tsx)

The main page is where the entire process starts. Since it is a Server Component, we can make it an async function and call our server-side data fetching utility directly.

// src/app/page.tsx

import { getPosts } from '@/lib/posts';
import BlogPost from '@/components/BlogPost';

// The 'async' keyword allows direct server-side data fetching
export default async function HomePage() {
  const posts = await getPosts(); // Data fetched BEFORE the component renders

  return (
    <main className="container mx-auto px-4 py-8 max-w-3xl">
      <h1 className="text-5xl font-extrabold text-center text-gray-900 mb-12">
        My Awesome RSC Blog
      </h1>
      <section>
        {posts.map((post) => (
          <BlogPost key={post.id} post={post} />
        ))}
      </section>
    </main>
  );
}

Conclusion: Performance by Default

By using Next.js Server Components, we achieve three major benefits:

  1. Zero-Bundle Static Content: The vast majority of the blog post text is rendered to HTML on the server, avoiding client-side JavaScript download for the content.

  2. Efficient Data Fetching: Data retrieval happens close to the source (the server), eliminating the need for client-side API roundtrips and complex state management (useEffect, loaders).

  3. Clean Separation of Concerns: We clearly separate server logic (data, secrets) from client logic (state, events), leading to cleaner, more maintainable code.

This pattern—Server Components for content and data, Client Components for interaction—is the blueprint for building performant, modern web applications.

Useful links below:

Let me & my team build you a money making website/blog for your business https://bit.ly/tnrwebsite_service

Get Bluehost hosting for as little as $1.99/month (save 75%)…https://bit.ly/3C1fZd2

Best email marketing automation solution on the market! http://www.aweber.com/?373860

Build high converting sales funnels with a few simple clicks of your mouse! https://bit.ly/484YV29

Join my Patreon for one-on-one coaching and help with your coding…https://www.patreon.com/c/TyronneRatcliff

Buy me a coffee ☕️https://buymeacoffee.com/tyronneratcliff

{ 1 comment… add one }

Leave a Comment