Building a Server-Side Rendered Blog with Next.js: A Complete Tutorial

In the dynamic world of web development, creating fast, SEO-friendly, and engaging user experiences is paramount. Server-side rendering (SSR) has emerged as a crucial technique to achieve these goals. This is where Next.js shines. Next.js, a React framework, simplifies the process of building server-rendered applications, making it easier for developers of all levels to create high-performance websites and web applications. This tutorial will guide you through the process of building a blog using Next.js, covering everything from project setup to deployment.

Why Server-Side Rendering Matters

Before diving into the code, let’s understand why SSR is so important, especially for a blog. Traditional client-side rendered (CSR) websites load a blank page initially, and then the JavaScript downloads, parses, and renders the content. This can lead to a slower initial load time, negatively impacting user experience and SEO. Search engine crawlers also struggle to index CSR websites effectively, as they might not execute JavaScript, leading to content not being discovered.

SSR addresses these issues by rendering the website’s content on the server and sending the fully rendered HTML to the client. This results in faster initial load times, improved SEO, and a better user experience. Next.js makes SSR straightforward, allowing you to focus on building your blog’s content and features.

Prerequisites

To follow this tutorial, you should have a basic understanding of HTML, CSS, and JavaScript. Familiarity with React is beneficial but not strictly required. You’ll also need:

  • Node.js and npm (or yarn) installed on your machine.
  • A code editor (e.g., VS Code, Sublime Text).
  • Basic knowledge of the command line.

Setting Up Your Next.js Project

Let’s start by creating a new Next.js project. Open your terminal and run the following command:

npx create-next-app my-nextjs-blog
cd my-nextjs-blog

This command sets up a new Next.js project named “my-nextjs-blog”. Navigate into the project directory using the “cd” command.

Project Structure Overview

Next.js projects have a specific directory structure that helps organize your code. Here’s a basic overview:

  • pages/: This is where you’ll create your pages. Each file in this directory becomes a route in your application. For example, pages/index.js becomes the homepage (/), and pages/about.js becomes the about page (/about).
  • public/: This directory is for static assets like images, fonts, and other files that you want to serve directly.
  • styles/: This directory is a common place to put your CSS or other styling files.
  • components/: A good place to put reusable React components.
  • next.config.js: Configuration file for Next.js.
  • package.json: Lists your project dependencies and scripts.

Creating the Homepage (index.js)

The pages/index.js file is the entry point for your blog’s homepage. Let’s modify this file to display some basic content. Open pages/index.js and replace the existing code with the following:

import Head from 'next/head';

export default function Home() {
  return (
    <div className="container">
      <Head>
        <title>My Next.js Blog</title>
        <meta name="description" content="A blog built with Next.js" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className="main">
        <h1>Welcome to My Blog</h1>
        <p>This is the homepage of my Next.js blog.</p>
      </main>

      <footer className="footer">
        <a
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by{' '}
          <img src="/vercel.svg" alt="Vercel Logo" className="logo" />
        </a>
      </footer>
    </div>
  )
}

This code imports the Head component from Next.js, which allows you to set the <title> and <meta> tags for your page. The rest of the code defines the basic HTML structure for your homepage, including a title, description, and some introductory text. We’ve also added a basic footer. You’ll notice we’re using CSS classes like “container”, “main”, and “footer”. Let’s define these styles.

Styling the Homepage

Create a file named styles/globals.css (if it doesn’t already exist) and add the following CSS rules:

html,
body {
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}

a {
  color: inherit;
  text-decoration: none;
}

* {
  box-sizing: border-box;
}

.container {
  min-height: 100vh;
  padding: 0 0.5rem;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.main {
  padding: 5rem 0;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.footer {
  width: 100%;
  height: 100px;
  border-top: 1px solid #eaeaea;
  display: flex;
  justify-content: center;
  align-items: center;
}

.footer a {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-grow: 1;
}

.title a {
  color: #0070f3;
  text-decoration: none;
}

.title a:hover,
.title a:focus,
.title a:active {
  text-decoration: underline;
}

.title {
  margin: 0;
  line-height: 1.15;
  font-size: 4rem;
}

.title,
.description {
  text-align: center;
}

.description {
  line-height: 1.5;
  font-size: 1.5rem;
}

.code {
  background: #fafafa;
  border-radius: 5px;
  padding: 0.75rem;
  font-size: 1.1rem;
  font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace;
}

.grid {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  max-width: 800px;
  margin-top: 3rem;
}

.card {
  margin: 1rem;
  padding: 1.5rem;
  text-align: left;
  color: inherit;
  text-decoration: none;
  border: 1px solid #eaeaea;
  border-radius: 10px;
  transition: color 0.15s ease, border-color 0.15s ease;
  width: 100%;
  max-width: 300px;
}

.card:hover,
.card:focus,
.card:active {
  color: #0070f3;
  border-color: #0070f3;
}

.card h2 {
  margin: 0 0 1rem 0;
  font-size: 1.5rem;
}

.card p {
  margin: 0;
  font-size: 1.25rem;
  line-height: 1.5;
}

.logo {
  height: 1em;
  margin-left: 0.5rem;
}

Import the CSS file into your pages/_app.js file to make the styles available globally. If the file doesn’t exist, create it. This file is used to initialize pages. Add the following code:

import '../styles/globals.css'

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} /gt;
}

export default MyApp

Running Your Application

Now, start the development server by running the following command in your terminal:

npm run dev  # or yarn dev

This will start the development server, usually on http://localhost:3000. Open this address in your browser, and you should see your basic homepage. Congratulations, you’ve set up your Next.js blog!

Creating Blog Posts (Dynamic Routes)

A core feature of any blog is the ability to create and display individual blog posts. Next.js makes this easy with dynamic routes. We’ll create a simple system where each blog post is a separate file, and Next.js automatically generates the routes based on the filenames.

First, create a directory called posts inside your pages directory. This is where we’ll store the individual blog post files. Inside the posts directory, create a file named [slug].js. The square brackets indicate that this is a dynamic route. The `slug` part represents a unique identifier for each blog post (e.g., the title, converted to a URL-friendly format).

// pages/posts/[slug].js
import { useRouter } from 'next/router';
import Head from 'next/head';

export default function Post() {
  const router = useRouter();
  const { slug } = router.query;

  // Replace this with your data fetching logic
  const postTitle = slug ? slug.replace(/-/g, ' ') : 'Loading...'; // Simple example

  return (
    <div>
      <Head>
        <title>{postTitle}</title>
      </Head>
      <h1>{postTitle}</h1>
      <p>This is the content of the post with slug: {slug}</p>
    </div>
  );
}

Let’s break down this code:

  • useRouter: This hook from Next.js provides access to the router object, which contains information about the current route.
  • router.query: This object contains the query parameters from the URL. In our case, it will contain the `slug` value.
  • slug: This variable holds the value from the URL (e.g., “my-first-post” if the URL is /posts/my-first-post).

The code currently just displays the slug in the title and content. In a real application, you would fetch the actual content of the blog post based on the slug from a data source (e.g., a file, a database, or a CMS). We’ll cover data fetching in the next section.

Fetching Blog Post Data

To display the actual content of a blog post, you need to fetch the data. Next.js provides several ways to do this, including:

  • getStaticProps: Used for statically generated pages. This is ideal for blog posts where the content doesn’t change frequently. The data is fetched at build time.
  • getServerSideProps: Used for server-side rendered pages. This fetches the data on each request. Useful for content that changes frequently or that requires user-specific data.
  • Client-side data fetching (using useEffect and fetch): Useful for less critical data or when you need to update data dynamically.

For this tutorial, let’s use getStaticProps, as it’s the most efficient approach for static blog posts.

First, create a simple utility function to simulate fetching blog post data. Create a file named lib/posts.js and add the following code:

// lib/posts.js
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter'; // npm install gray-matter

const postsDirectory = path.join(process.cwd(), 'posts');

export function getPostFiles() {
  return fs.readdirSync(postsDirectory);
}

export function getPostData(postIdentifier) {
  const postSlug = postIdentifier.replace(/.md$/, ''); // removes the file extension
  const filePath = path.join(postsDirectory, `${postSlug}.md`);
  const fileContent = fs.readFileSync(filePath, 'utf-8');
  const { data, content } = matter(fileContent);

  const postData = {
    slug: postSlug,
    ...data,
    content,
  };

  return postData;
}

export function getAllPosts() {
  const postFiles = getPostFiles();

  const allPosts = postFiles.map(postFile => {
    return getPostData(postFile);
  });

  return allPosts.sort((postA, postB) => postA.date < postB.date ? 1 : -1);
}

This code does the following:

  • Imports necessary modules: fs (file system) and path (path manipulation) from Node.js, and gray-matter to parse markdown files. You’ll need to install gray-matter: npm install gray-matter.
  • Defines postsDirectory, which points to the location of your blog post files.
  • getPostFiles(): Reads the filenames within the posts directory.
  • getPostData(postIdentifier): Reads the content of a single post file, parses it using gray-matter, and returns an object containing the post’s metadata (e.g., title, date) and content.
  • getAllPosts(): Retrieves all blog posts, sorts them by date (newest first), and returns an array of post data.

Now, let’s create a simple Markdown file for a blog post. Create a file named posts/my-first-post.md (or any name you like) and add some content. Here’s an example:


---
title: My First Blog Post
date: 2024-01-26
---

# Welcome to My Blog!

This is the content of my first blog post.  I'm excited to share my thoughts and experiences.

Here are some things I might write about:

*   Next.js
*   React
*   Web Development

This file uses a frontmatter section (the lines between the ---) to store metadata (title, date). The rest of the file contains the Markdown content. The `gray-matter` package will parse this frontmatter and content for us.

Now, modify pages/posts/[slug].js to use getStaticProps to fetch the post data. Here’s the updated code:

// pages/posts/[slug].js
import { useRouter } from 'next/router';
import Head from 'next/head';
import { getPostData } from '../../lib/posts';
import ReactMarkdown from 'react-markdown'; // npm install react-markdown

export async function getStaticPaths() {
  // This function is used to dynamically generate the paths for all posts.
  // In a real application, you would get these from your data source.
  return {
    paths: [], // No paths are generated at build time because we're using dynamic routing
    fallback: 'blocking', // or true, or false.  'blocking' is recommended for SSR
  };
}

export async function getStaticProps(context) {
  const { params } = context;
  const { slug } = params;
  const postData = getPostData(slug);

  return {
    props: {
      postData,
    },
    revalidate: 60, // In seconds, optional: re-generate the page at most once every 60 seconds
  };
}

export default function Post({ postData }) {
  const router = useRouter();
  const { slug } = router.query;

  if (!postData) {
    return <div>Loading...</div>; // Or handle the case where the post isn't found
  }

  return (
    <div>
      <Head>
        <title>{postData.title}</title>
      </Head>
      <h1>{postData.title}</h1>
      <p>{postData.date}</p>
      <ReactMarkdown>{postData.content}</ReactMarkdown>
    </div>
  );
}

Let’s break down the changes:

  • We import getPostData from ../../lib/posts.
  • We import ReactMarkdown. Install it with: npm install react-markdown
  • getStaticPaths: This function is required when using dynamic routes with getStaticProps. It tells Next.js which paths (URLs) to pre-render at build time. Because we’re using a dynamic route, we need to tell Next.js which slugs to build. In this example, we return an empty array for paths and set fallback: 'blocking'. This means that Next.js will generate the page on the first request and cache it. Other options for fallback include true (shows a fallback page while generating) or false (shows a 404 if the path isn’t known at build time).
  • getStaticProps: This function fetches the data for a specific post based on the slug parameter. It calls getPostData from lib/posts.js to get the post data. The return value is an object with a props property, which contains the data that will be passed to the component. We’ve also included an optional revalidate setting. This tells Next.js to regenerate the page at most once every 60 seconds, which is useful for content that might change.
  • Inside the component, we access the postData prop and display the title and content. We use the ReactMarkdown component to render the Markdown content.

Now, when you navigate to a URL like /posts/my-first-post, Next.js will generate the page using the data fetched by getStaticProps.

Listing Blog Posts (index.js – Revisited)

The homepage (pages/index.js) should now display a list of all your blog posts, with links to each individual post. Modify pages/index.js to display a list of posts. Replace the contents with the following code:

// pages/index.js
import Head from 'next/head';
import Link from 'next/link';
import { getAllPosts } from '../lib/posts';

export async function getStaticProps() {
  const allPosts = getAllPosts();

  return {
    props: {
      allPosts,
    },
  };
}

export default function Home({ allPosts }) {
  return (
    <div className="container">
      <Head>
        <title>My Next.js Blog</title>
        <meta name="description" content="A blog built with Next.js" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className="main">
        <h1>Welcome to My Blog</h1>
        <p>Check out my latest blog posts:</p>

        <ul>
          {allPosts.map((post) => (
            <li key={post.slug}>
              <Link href={`/posts/${post.slug}`}>
                <a>{post.title}</a>
              </Link>
            </li>
          ))}
        </ul>
      </main>

      <footer className="footer">
        <a
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by{' '}
          <img src="/vercel.svg" alt="Vercel Logo" className="logo" />
        </a>
      </footer>
    </div>
  );
}

Let’s examine the changes:

  • We import Link from next/link to create links to the individual blog posts.
  • We import getAllPosts from ../lib/posts.
  • We define a getStaticProps function to fetch all posts using getAllPosts(). The returned data is available as props to the component.
  • We map over the allPosts array and render a list of links to each post. The href attribute of the Link component is set to /posts/${post.slug}, which matches the dynamic route we created earlier.

Now, your homepage will display a list of your blog posts, and clicking on a post title will take you to the corresponding post page.

Adding More Blog Posts

To add more blog posts, simply create new Markdown files in the posts directory. Make sure each file has a unique filename (e.g., my-second-post.md) and includes the necessary frontmatter.

Common Mistakes and Troubleshooting

Here are some common mistakes and how to fix them:

  • Incorrect File Paths: Double-check your file paths, especially when importing modules or images.
  • Missing Dependencies: Ensure you’ve installed all the necessary dependencies (e.g., gray-matter, react-markdown). Run npm install or yarn install to install dependencies.
  • Typographical Errors: Carefully review your code for typos, especially in variable names and component names.
  • Incorrect Route Definitions: Ensure your file names in the pages directory are correctly named to create the desired routes. Remember, square brackets indicate dynamic routes.
  • Caching Issues: If you’ve made changes to your data source (e.g., added a new blog post) and the changes aren’t reflected, try restarting your development server (npm run dev) and clearing your browser’s cache. You can also use the revalidate option in getStaticProps to automatically update the page.
  • Markdown Rendering Issues: Make sure you’ve installed react-markdown and that you’re using it correctly to render your Markdown content.
  • 404 Errors: If you’re getting 404 errors, double-check your routes and make sure the file paths are correct. Also, verify that getStaticPaths is correctly implemented if you’re using dynamic routes.

SEO Best Practices

Here are some SEO best practices to consider for your Next.js blog:

  • Meta Tags: Use the Head component to set the <title> and <meta> tags for each page. Include a descriptive title and meta description for each blog post. Keywords are important.
  • Semantic HTML: Use semantic HTML elements (e.g., <h1>, <h2>, <p>, <article>, <aside>) to structure your content logically.
  • Image Optimization: Optimize images for web use. Use the next/image component for efficient image loading and performance.
  • Structured Data (Schema Markup): Implement structured data using schema.org markup to provide search engines with more information about your content (e.g., article titles, author information, dates). You can use JSON-LD for this.
  • XML Sitemap: Generate an XML sitemap to help search engines discover and index your content. You can use a library like next-sitemap.
  • Robots.txt: Create a robots.txt file to control how search engine crawlers interact with your site.
  • Mobile-First Design: Ensure your blog is responsive and provides a good user experience on all devices.
  • Internal Linking: Link to other relevant content within your blog to improve user engagement and SEO.
  • Keyword Research: Conduct keyword research to identify relevant keywords for your blog posts and incorporate them naturally into your content.

Deploying Your Blog

Next.js is designed for easy deployment. You can deploy your blog to various platforms, including:

  • Vercel: Vercel is the recommended platform for Next.js applications. It provides seamless deployment, automatic builds, and a global CDN. To deploy to Vercel, simply push your code to a Git repository (e.g., GitHub, GitLab, Bitbucket) and connect your repository to Vercel.
  • Netlify: Netlify is another popular platform that offers similar features to Vercel.
  • Other Platforms: You can also deploy to platforms like AWS, Google Cloud, or other hosting providers.

Deploying to Vercel is straightforward. After connecting your Git repository, Vercel will automatically build and deploy your application. It will also handle the server-side rendering and static asset serving.

Summary / Key Takeaways

This tutorial has provided a comprehensive guide to building a server-side rendered blog using Next.js. You’ve learned how to set up a Next.js project, create dynamic routes for blog posts, fetch and display data using getStaticProps, and implement basic styling. You’ve also learned about the importance of SEO and how to optimize your blog for search engines.

FAQ

  1. Can I use a database with Next.js? Yes, you can. You can use any database (e.g., MongoDB, PostgreSQL, MySQL) with Next.js. You’ll typically interact with the database in your getStaticProps or getServerSideProps functions.
  2. How do I handle user authentication? You can implement user authentication using various methods, such as JWT (JSON Web Tokens), OAuth, or third-party services like Firebase Authentication or Auth0.
  3. How can I add comments to my blog posts? You can use a third-party commenting service like Disqus or integrate a self-hosted commenting system.
  4. How do I add pagination to my blog? You’ll need to modify your getAllPosts function to return a subset of posts based on the current page number and the number of posts per page. You’ll also need to add pagination controls (e.g., “Next” and “Previous” buttons) to your homepage.
  5. How can I add a search function? You can implement a search function by allowing the user to input a search query and then filtering the blog posts based on the query. You can do this either on the client-side or server-side.

Building a blog with Next.js is a rewarding experience. As you delve deeper into the framework, you’ll discover a wealth of features and capabilities that can help you create a highly performant, SEO-friendly, and user-friendly blog. Remember to experiment, iterate, and continuously refine your blog to provide value to your readers and achieve your online goals. The power of Next.js lies in its flexibility and its ability to empower developers to build exceptional web experiences. By combining its features with the principles of good design and SEO, you can create a blog that not only looks great but also attracts and retains a dedicated audience.