Next.js & TypeScript: Building a Blog with Dynamic Content

In the ever-evolving landscape of web development, creating dynamic, content-rich websites that are both performant and maintainable is a constant challenge. Next.js, a powerful React framework, provides a robust solution. Coupled with TypeScript, a superset of JavaScript that adds static typing, Next.js empowers developers to build scalable, type-safe applications with ease. This tutorial will guide you through the process of building a dynamic blog using Next.js and TypeScript. We’ll explore how to fetch data, render content dynamically, and ensure type safety throughout the development process. You’ll learn how to leverage the strengths of both technologies to create a modern, efficient, and enjoyable blogging experience.

Why Next.js and TypeScript?

Before diving into the code, let’s understand why Next.js and TypeScript are a winning combination for building a blog. Next.js excels in several areas:

  • Performance: Next.js offers features like Server-Side Rendering (SSR) and Static Site Generation (SSG), which significantly improve website performance and SEO.
  • Developer Experience: Features like built-in routing, image optimization, and API routes streamline development, making it faster and more enjoyable.
  • Scalability: Next.js is designed to handle large-scale applications, making it ideal for blogs that may grow over time.

TypeScript brings a wealth of benefits to the table:

  • Type Safety: TypeScript helps catch errors during development, reducing the likelihood of runtime bugs.
  • Code Completion and Refactoring: TypeScript provides excellent code completion and refactoring capabilities, improving developer productivity.
  • Maintainability: Type annotations make code easier to understand and maintain, especially in larger projects.

By combining these two technologies, you create a powerful, efficient, and maintainable blogging platform.

Setting Up Your Project

Let’s get started by setting up a new Next.js project with TypeScript. Open your terminal and run the following command:

npx create-next-app@latest my-blog-with-ts --typescript

This command creates a new Next.js project named “my-blog-with-ts” and configures it to use TypeScript. Navigate into your project directory:

cd my-blog-with-ts

Now, let’s install any dependencies we’ll need for this project. We’ll use a simple Markdown parser and a library to fetch data from a hypothetical API. Run the following command:

npm install gray-matter remark remark-html

These libraries will help us parse Markdown files and render them as HTML. We will also create a basic folder structure to organize our components and data.

mkdir components pages posts

Fetching and Displaying Blog Posts

The core of any blog is its content. We’ll simulate fetching blog posts from a data source. In a real-world scenario, this might be a database, a content management system (CMS), or a static file system. For this example, we’ll create simple mock data.

Creating Mock Data

Create a directory called `posts` inside the `posts` directory. Inside this directory, create some Markdown files (e.g., `first-post.md`, `second-post.md`). Each Markdown file will represent a blog post.

Here’s an example of `first-post.md`:


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

# Welcome to my blog!

This is the first post on my new blog, built with Next.js and TypeScript.

I'm excited to share my thoughts and experiences with you.

And here is an example of `second-post.md`:


---
title: "Another Great Post"
date: "2024-01-27"
---

# Another awesome blog post!

This is my second post on my new blog, built with Next.js and TypeScript.

Hope you are enjoying the content!

Now let’s create a utility function to read these posts. Create a file named `getPosts.ts` inside the `posts` directory. This file will contain functions to fetch and process our mock blog post data.


import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { remark } from 'remark';
import html from 'remark-html';

export interface PostData {
  title: string;
  date: string;
  content: string;
  slug: string;
}

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

export async function getSortedPostsData() {
  // Get file names under /posts
  const fileNames = fs.readdirSync(postsDirectory);
  const allPostsData = await Promise.all(
    fileNames.map(async (fileName) => {
      // Remove ".md" from file name to get id
      const slug = fileName.replace(/.md$/, '');

      // Read markdown file as string
      const fullPath = path.join(postsDirectory, fileName);
      const fileContents = fs.readFileSync(fullPath, 'utf8');

      // Use gray-matter to parse the post metadata section
      const matterResult = matter(fileContents);

      // Use remark to convert markdown into HTML
      const processedContent = await remark()
        .use(html)
        .process(matterResult.content);
      const content = processedContent.toString();

      // Combine the data with the id
      return {
        slug,
        content,
        ...(matterResult.data as {
          title: string;
          date: string;
        }),
      };
    })
  );
  // Sort posts by date
  return allPostsData.sort((a, b) => {
    if (a.date < b.date) {
      return 1;
    } else {
      return -1;
    }
  });
}

export async function getPostData(slug: string) {
  const fullPath = path.join(postsDirectory, `${slug}.md`);
  const fileContents = fs.readFileSync(fullPath, 'utf8');

  // Use gray-matter to parse the post metadata section
  const matterResult = matter(fileContents);

  // Use remark to convert markdown into HTML
  const processedContent = await remark()
    .use(html)
    .process(matterResult.content);
  const content = processedContent.toString();

  // Combine the data with the id
  return {
    slug,
    content,
    ...(matterResult.data as {
      title: string;
      date: string;
    }),
  };
}

This code does the following:

  • Imports necessary modules for file system operations, Markdown parsing, and HTML conversion.
  • Defines a `PostData` interface to specify the structure of our blog post data.
  • `getSortedPostsData`: Reads all `.md` files in the `posts` directory, parses their metadata, converts the Markdown content to HTML, and returns the sorted data as an array of `PostData` objects.
  • `getPostData`: Reads a specific `.md` file by its slug, parses the metadata, converts the Markdown content to HTML, and returns the post data.

Creating a Post Listing Page

Now, let’s create a page to display a list of all blog posts. Open `pages/index.tsx` and replace the existing code with the following:


import { GetStaticProps } from 'next';
import Link from 'next/link';
import { getSortedPostsData, PostData } from '../posts/getPosts';

interface Props {
  allPostsData: PostData[];
}

export default function Home({ allPostsData }: Props) {
  return (
    <div>
      <h1>Blog Posts</h1>
      <ul>
        {allPostsData.map(({ slug, title, date }) => (
          <li>
            {title} <br />
            <small>{date}</small>
          </li>
        ))}
      </ul>
    </div>
  );
}

export const getStaticProps: GetStaticProps = async () => {
  const allPostsData = await getSortedPostsData();
  return {
    props: {
      allPostsData,
    },
  };
};

In this code:

  • We import `getSortedPostsData` from `../posts/getPosts`.
  • Define an interface `Props` for type safety.
  • Inside the `Home` component, we map over the `allPostsData` to display a list of links to each post. We use the `Link` component from `next/link` for client-side navigation.
  • `getStaticProps` is a Next.js function that fetches data at build time. We use it to get the post data and pass it as props to the `Home` component.

Creating a Post Detail Page

Next, let’s create a dynamic route to display the content of each blog post. Create a file named `pages/posts/[slug].tsx`.


import { GetStaticProps, GetStaticPaths } from 'next';
import { getPostData, getSortedPostsData, PostData } from '../../posts/getPosts';
import React from 'react';

interface Props {
  postData: PostData;
}

export default function Post({ postData }: Props) {
  return (
    <div>
      <h1>{postData.title}</h1>
      <small>{postData.date}</small>
      <div />
    </div>
  );
}

export const getStaticPaths: GetStaticPaths = async () => {
  const posts = await getSortedPostsData();

  const paths = posts.map((post) => ({
    params: {
      slug: post.slug,
    },
  }));

  return {
    paths,
    fallback: false,
  };
};

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const postData = await getPostData(params?.slug as string);
  return {
    props: {
      postData,
    },
  };
};

In this code:

  • We import `getPostData` and `getSortedPostsData` from `../../posts/getPosts`.
  • `getStaticPaths` is a Next.js function that specifies all the possible paths for this dynamic route at build time. It fetches all the posts and creates a path for each one, using the slug as the route parameter.
  • `getStaticProps` fetches the data for a specific post based on the `slug` parameter and passes it as props to the `Post` component.
  • The `Post` component then renders the post’s title, date, and content. We use `dangerouslySetInnerHTML` to render the HTML content generated from the Markdown. Make sure you sanitize the content if it comes from an untrusted source.

Styling Your Blog

While the basic functionality is in place, let’s add some styling to make your blog visually appealing. Next.js supports various styling solutions, including CSS Modules, Styled Components, and Tailwind CSS. For simplicity, we’ll use basic CSS Modules in this example.

Creating a Stylesheet

Create a file named `styles/global.module.css` in your project’s `styles` directory. This will hold your global styles.


body {
  font-family: sans-serif;
  margin: 20px;
}

h1 {
  font-size: 2em;
  margin-bottom: 10px;
}

li {
  margin-bottom: 10px;
}

Importing Styles

Import the stylesheet in your `pages/_app.tsx` file (or `pages/_app.js` if you are not using TypeScript):


import '../styles/global.module.css';

function MyApp({ Component, pageProps }: any) {
  return ;
}

export default MyApp;

This ensures that your global styles are applied to all pages.

Styling the Post List

Let’s add some basic styling to the post list in `pages/index.tsx`. Create a new CSS Module file called `styles/Home.module.css` and add the following styles:


.container {
  max-width: 800px;
  margin: 0 auto;
}

.postLink {
  display: block;
  margin-bottom: 10px;
  text-decoration: none;
  color: #333;
}

.postLink:hover {
  color: #0070f3;
}

.date {
  font-size: 0.8em;
  color: #666;
}

Then, import and use these styles in `pages/index.tsx`:


import { GetStaticProps } from 'next';
import Link from 'next/link';
import { getSortedPostsData, PostData } from '../posts/getPosts';
import styles from '../styles/Home.module.css';

interface Props {
  allPostsData: PostData[];
}

export default function Home({ allPostsData }: Props) {
  return (
    <div>
      <h1>Blog Posts</h1>
      <ul>
        {allPostsData.map(({ slug, title, date }) => (
          <li>
            
              {title}
            
            <br />
            <small>{date}</small>
          </li>
        ))}
      </ul>
    </div>
  );
}

export const getStaticProps: GetStaticProps = async () => {
  const allPostsData = await getSortedPostsData();
  return {
    props: {
      allPostsData,
    },
  };
};

We import the styles using `import styles from ‘../styles/Home.module.css’;` and then apply the classes to the relevant elements using `className={styles.container}`, `className={styles.postLink}`, and `className={styles.date}`.

Styling the Post Detail Page

Similarly, let’s style the post detail page. Create a file `styles/Post.module.css` with the following styles:


.container {
  max-width: 800px;
  margin: 0 auto;
}

.date {
  font-size: 0.8em;
  color: #666;
}

.content {
  margin-top: 20px;
}

Then, import and use these styles in `pages/posts/[slug].tsx`:


import { GetStaticProps, GetStaticPaths } from 'next';
import { getPostData, getSortedPostsData, PostData } from '../../posts/getPosts';
import React from 'react';
import styles from '../../styles/Post.module.css';

interface Props {
  postData: PostData;
}

export default function Post({ postData }: Props) {
  return (
    <div>
      <h1>{postData.title}</h1>
      <small>{postData.date}</small>
      <div />
    </div>
  );
}

export const getStaticPaths: GetStaticPaths = async () => {
  const posts = await getSortedPostsData();

  const paths = posts.map((post) => ({
    params: {
      slug: post.slug,
    },
  }));

  return {
    paths,
    fallback: false,
  };
};

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const postData = await getPostData(params?.slug as string);
  return {
    props: {
      postData,
    },
  };
};

This adds basic styling to the post detail page.

Common Mistakes and How to Fix Them

Building a Next.js blog with TypeScript is generally straightforward, but you might encounter a few common issues.

1. TypeScript Errors

TypeScript errors are common, especially when you’re starting. The compiler will catch type-related issues. For example, if you try to pass a string to a function that expects a number, TypeScript will flag the error. Make sure to read the error messages carefully as they usually give you clues on how to fix the errors.

Fix: Carefully review the error messages. Ensure your types are correctly defined and that you’re passing the correct data types to your functions and components. Use type annotations (`: string`, `: number`, etc.) to explicitly define the expected types. Use interfaces and types for complex object structures. Consider using `as` to cast types when necessary.

2. `getStaticProps` and `getStaticPaths` Issues

These functions are crucial for static site generation. Common problems include:

  • Incorrect Data Fetching: Make sure your data fetching logic within `getStaticProps` and `getStaticPaths` is working correctly. Test your data fetching functions separately.
  • Incorrect Path Generation: In `getStaticPaths`, ensure you’re generating the correct paths for your dynamic routes. The `params` object needs to match the route structure (e.g., `[slug]` in `pages/posts/[slug].tsx`).
  • Missing or Incorrect `fallback` Value: The `fallback` option in `getStaticPaths` is important. If you set it to `false`, any paths not generated at build time will result in a 404 error. If you set it to `true`, Next.js will generate the page on-demand when a user visits a path that wasn’t generated at build time. If you set it to `blocking`, Next.js will wait for the page to be generated before showing it to the user.

Fix: Carefully review the console for any errors related to data fetching or path generation. Use `console.log` to inspect the data and paths being generated. Double-check your file paths and ensure your data fetching functions are returning the expected data. Make sure to choose the correct value for the `fallback` option based on your needs.

3. Rendering Markdown Incorrectly

When rendering Markdown content, it’s easy to make mistakes that prevent the content from displaying correctly.

  • Missing Markdown Parser: Ensure you have a Markdown parser library installed (e.g., `gray-matter`, `remark`, `remark-html`).
  • Incorrect HTML Rendering: Use `dangerouslySetInnerHTML` to render the HTML generated from your Markdown, but be cautious of potential security risks (sanitize the content if it comes from an untrusted source).
  • Incorrect Paths for Images: If your Markdown includes images, ensure the paths to the images are correct relative to your Markdown files.

Fix: Verify that your Markdown parsing library is installed and correctly configured. Double-check that you’re using `dangerouslySetInnerHTML` correctly and that the HTML is being generated as expected. Test your Markdown files in isolation to ensure they are being parsed correctly. If you’re using images, double-check the image paths.

4. CSS Module Issues

CSS Modules are a great way to scope your styles, but they can be tricky to set up and use initially.

  • Incorrect Imports: Make sure you’re importing the CSS module correctly (e.g., `import styles from ‘./MyComponent.module.css’;`).
  • Incorrect Class Names: Ensure you’re using the correct class names from your CSS module (e.g., `className={styles.myClass}`).
  • Specificity Issues: CSS Modules have specific scoping rules. If you find that your styles are not being applied, you might need to adjust your CSS selectors or the order in which you import your CSS files.

Fix: Double-check that you’re importing the correct CSS module path. Verify that you’re using the correct class names from the module, including the `styles.` prefix. If you’re having specificity issues, consider using more specific CSS selectors or adjusting the order in which you import your CSS files. Make sure your CSS Modules are correctly configured in your Next.js project.

Key Takeaways

  • Next.js and TypeScript provide a powerful combination for building modern, performant, and maintainable blogs.
  • Next.js offers excellent features for performance, developer experience, and scalability.
  • TypeScript enhances code quality, reduces bugs, and improves maintainability.
  • Using `getStaticProps` and `getStaticPaths` is crucial for static site generation.
  • CSS Modules offer a convenient way to style your components.

FAQ

1. Can I use a database instead of Markdown files?

Yes, absolutely! While this tutorial uses Markdown files for simplicity, you can easily integrate a database (e.g., MongoDB, PostgreSQL) or a CMS (e.g., Strapi, Contentful) to store and manage your blog posts. You would simply modify the data fetching functions (`getSortedPostsData` and `getPostData`) to fetch data from your chosen data source.

2. How can I add pagination to my blog?

Pagination is a common feature for blogs with a large number of posts. You can implement pagination by modifying the `getStaticProps` function in `pages/index.tsx`. Instead of fetching all posts at once, you would fetch a limited number of posts per page and calculate the total number of pages. You would then pass the current page number and the total number of pages as props to your component. You would also need to create pagination links to allow users to navigate between pages.

3. How do I deploy this blog?

Next.js makes deployment easy. You can deploy your blog to various platforms, including Vercel (recommended), Netlify, or AWS. Vercel is particularly well-suited for Next.js projects, offering automatic deployments, CDN integration, and other features. To deploy to Vercel, you would typically connect your Git repository to Vercel, and Vercel will automatically build and deploy your blog whenever you push changes to your repository.

4. How can I add comments to my blog posts?

Adding comments involves integrating a third-party commenting service (e.g., Disqus, Facebook Comments) or building your own commenting system. For a third-party service, you would typically include a script provided by the service in your post detail page. For a custom solution, you would need to implement a backend to store and manage comments.

Next Steps

Building a dynamic blog with Next.js and TypeScript is a rewarding project. This tutorial has provided a solid foundation. From here, you can expand on this by adding features such as categories, tags, author information, search functionality, and a more sophisticated design. Explore advanced Next.js features like Image Optimization, API Routes for handling user interactions, and more. With Next.js and TypeScript, the possibilities are vast, empowering you to create a professional, engaging, and highly functional blog.