Next.js & TypeScript: Building a Blog with Markdown

In the ever-evolving landscape of web development, creating a blog that’s both performant and easily manageable is a common challenge. Traditional content management systems (CMS) can be cumbersome, and hand-coding HTML can be time-consuming and difficult to maintain. This is where the power of Next.js, combined with TypeScript and Markdown, shines. This tutorial will guide you through building a blog where content is written in Markdown, converted to HTML, and displayed with the speed and efficiency that Next.js is known for. We’ll explore how to set up your project, parse Markdown files, and dynamically generate blog posts, ensuring a seamless experience for both you and your readers.

Why Choose Next.js, TypeScript, and Markdown?

Before diving into the code, let’s understand why these technologies are a perfect match for building a blog:

  • Next.js: A React framework that enables server-side rendering (SSR), static site generation (SSG), and incremental static regeneration (ISR). This means your blog can be incredibly fast, with content served quickly to your users.
  • TypeScript: Adds static typing to JavaScript, helping you catch errors early in development and improving code maintainability. It provides better tooling and autocompletion, making your development process more efficient.
  • Markdown: A lightweight markup language that allows you to write content in a simple, readable format. It’s easy to learn and perfect for writing blog posts, documentation, and more.

By using these technologies, you’ll create a blog that’s easy to manage, fast-loading, and scalable. This combination allows for a great developer experience, too, with TypeScript helping to avoid common errors and Next.js providing a robust framework for building modern web applications.

Setting Up Your Next.js Project with TypeScript

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

npx create-next-app@latest my-markdown-blog --typescript

This command creates a new Next.js project in a directory called my-markdown-blog and configures it to use TypeScript. Navigate into your project directory:

cd my-markdown-blog

Next, install the necessary dependencies for parsing Markdown. We’ll use gray-matter to parse the front matter (metadata) and remark with remark-html to convert Markdown to HTML:

npm install gray-matter remark remark-html

With the project set up and dependencies installed, you’re ready to start building your blog.

Creating Your Markdown Blog Posts

Create a new directory called posts in the root of your project. This directory will store all your Markdown blog post files. Inside the posts directory, create a sample Markdown file, such as first-post.md. Here’s an example of what your Markdown file might look like:

---<br>title: "My First Blog Post"<br>date: 2024-01-26<br>author: John Doe<br>---<br><br># Hello, World!<br><br>This is my first blog post written in Markdown.<br><br>It's easy to learn and write content in this format.

The content above the --- lines is called front matter. It contains metadata about your blog post, such as the title, date, and author. This metadata will be used to display information about your post on your blog. The content below the front matter is the actual blog post content, formatted using Markdown syntax.

Parsing Markdown and Rendering Blog Posts

Now, let’s create a function to read and parse the Markdown files. Create a new file called lib/posts.ts in your project. This file will contain functions to read Markdown files, parse their content, and return data that can be used to render your blog posts.

import fs from 'fs';<br>import path from 'path';<br>import matter from 'gray-matter';<br>import { remark } from 'remark';<br>import html from 'remark-html';<br><br>export interface PostData {<br>  title: string;<br>  date: string;<br>  author: string;<br>  contentHtml: string;<br>  slug: string;<br>}<br><br>const postsDirectory = path.join(process.cwd(), 'posts');<br><br>export async function getSortedPostsData(): Promise<PostData[]> {<br>  // Get file names under /posts<br>  const fileNames = fs.readdirSync(postsDirectory);<br>  const allPostsData = await Promise.all(<br>    fileNames.map(async (fileName) => {<br>      // Remove ".md" from file name to get slug<br>      const slug = fileName.replace(/.md$/, '');<br>      // Read markdown file as string<br>      const fullPath = path.join(postsDirectory, fileName);<br>      const fileContents = fs.readFileSync(fullPath, 'utf8');<br><br>      // Use gray-matter to parse the post metadata section<br>      const matterResult = matter(fileContents);<br><br>      // Use remark to convert markdown into HTML string<br>      const processedContent = await remark()<br>        .use(html)<br>        .process(matterResult.content);<br>      const contentHtml = processedContent.toString();<br><br>      // Combine the data with the slug<br>      return {<br>        slug,<br>        contentHtml,<br>        ...(matterResult.data as { title: string; date: string; author: string }),<br>      } as PostData;<br>    })<br>  );<br>  // Sort posts by date<br>  return allPostsData.sort(({ date: a }, { date: b }) => {<br>    if (a < b) {<br>      return 1;<br>    }<br>    if (a > b) {<br>      return -1;<br>    }<br>    return 0;<br>  });<br>}<br><br>export async function getPostData(slug: string): Promise<PostData> {<br>  const fullPath = path.join(postsDirectory, `${slug}.md`);<br>  const fileContents = fs.readFileSync(fullPath, 'utf8');<br><br>  // Use gray-matter to parse the post metadata section<br>  const matterResult = matter(fileContents);<br><br>  // Use remark to convert markdown into HTML string<br>  const processedContent = await remark()<br>    .use(html)<br>    .process(matterResult.content);<br>  const contentHtml = processedContent.toString();<br><br>  // Combine the data with the slug<br>  return {<br>    slug,<br>    contentHtml,<br>    ...(matterResult.data as { title: string; date: string; author: string }),<br>  } as PostData;<br>}<br><br>export function getAllPostSlugs() {<br>  const fileNames = fs.readdirSync(postsDirectory);<br>  return fileNames.map((fileName) => {<br>    return {<br>      params: {<br>        slug: fileName.replace(/.md$/, ''),<br>      },<br>    };<br>  });<br>}<br>

Let’s break down the code:

  • Import Statements: Imports necessary modules for file system operations, path manipulation, Markdown parsing, and HTML conversion.
  • PostData Interface: Defines the structure of our post data, including the title, date, author, generated HTML content, and slug (the filename without the .md extension).
  • postsDirectory: Specifies the directory where your Markdown files are stored.
  • getSortedPostsData():
    • Reads all the filenames from the postsDirectory.
    • Uses gray-matter to parse the front matter from each file.
    • Uses remark and remark-html to convert the Markdown content to HTML.
    • Returns an array of PostData objects, sorted by date (newest first).
  • getPostData(slug: string):
    • Takes a slug (the filename without the .md extension) as an argument.
    • Reads the corresponding Markdown file.
    • Parses the front matter and converts the Markdown content to HTML using gray-matter and remark.
    • Returns a single PostData object for the specified post.
  • getAllPostSlugs():
    • Reads all the filenames from the postsDirectory.
    • Returns an array of objects, each containing the slug for a post. This is used for dynamic routes.

This code reads your Markdown files, extracts the metadata, converts the Markdown content to HTML, and provides the data in a format ready to be used in your Next.js components.

Creating the Blog Index Page

Now, let’s create the index page of your blog, which will display a list of all your blog posts. Open pages/index.tsx and modify it as follows:

import { GetStaticProps } from 'next';<br>import Link from 'next/link';<br>import { PostData, getSortedPostsData } from '../lib/posts';<br><br>interface Props {<br>  allPostsData: PostData[];<br>}<br><br>export default function Home({ allPostsData }: Props) {<br>  return (<br>    <div><br>      <h1>Blog Posts</h1><br>      <ul><br>        {allPostsData.map(({ slug, title, date, author }) => (<br>          <li key={slug}><br>            <Link href={`/posts/${slug}`}><br>              <a>{title}</a><br>            </Link><br>            <br /><br>            <small><br>              {date} by {author}<br>            </small><br>          </li><br>        ))}<br>      </ul><br>    </div><br>  );<br>}<br><br>export const getStaticProps: GetStaticProps<Props> = async () => {<br>  const allPostsData = await getSortedPostsData();<br>  return {<br>    props: {<br>      allPostsData,<br>    },<br>  };<br>};<br>

Here’s what the code does:

  • Imports: Imports necessary modules, including getSortedPostsData from lib/posts.ts and Link from next/link for navigation.
  • Props Interface: Defines the structure of the props that the component receives, which is an array of PostData objects.
  • Home Component:
    • Maps through the allPostsData array and renders a list of blog posts.
    • Each list item contains a link to the individual blog post page, using the slug for the URL.
    • Displays the title, date, and author of each post.
  • getStaticProps:
    • Uses getSortedPostsData to fetch the data for all blog posts.
    • Returns the data as props to the Home component. This makes it a statically generated page.

This code will fetch all your blog posts, display them as a list on the index page, and link to the individual post pages.

Creating the Blog Post Page

Next, let’s create the individual blog post pages. Create a new file in the pages/posts directory (create the directory if it doesn’t exist) named [slug].tsx. This is a dynamic route that will generate a page for each of your blog posts. Add the following code:

import { GetStaticProps, GetStaticPaths } from 'next';<br>import { PostData, getPostData, getAllPostSlugs } from '../../lib/posts';<br><br>interface Props {<br>  postData: PostData;<br>}<br><br>export default function Post({ postData }: Props) {<br>  return (<br>    <div><br>      <h1>{postData.title}</h1><br>      <p>{postData.date} by {postData.author}</p><br>      <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} /><br>    </div><br>  );<br>}<br><br>export const getStaticPaths: GetStaticPaths = async () => {<br>  const paths = getAllPostSlugs();<br>  return {<br>    paths,<br>    fallback: false,<br>  };<br>};<br><br>export const getStaticProps: GetStaticProps<Props> = async ({ params }) => {<br>  const postData = await getPostData(params?.slug as string);<br>  return {<br>    props: {<br>      postData,<br>    },<br>  };<br>};<br>

Let’s break down this code:

  • Imports: Imports necessary modules, including getPostData and getAllPostSlugs from lib/posts.ts.
  • Props Interface: Defines the structure of the props that the component receives, which is a single PostData object.
  • Post Component:
    • Renders the blog post content.
    • Displays the title, date, and author of the post.
    • Uses dangerouslySetInnerHTML to render the HTML content generated from Markdown. Important: Be careful when using this, as it can open you up to security vulnerabilities if the content is not properly sanitized. In this case, the content is generated from your trusted Markdown files, so it is safe.
  • getStaticPaths:
    • Uses getAllPostSlugs to get a list of all the slugs for your blog posts.
    • Returns an array of paths, which Next.js uses to pre-render the pages.
    • Sets fallback to false, which means any paths not returned by getStaticPaths will result in a 404.
  • getStaticProps:
    • Takes the params object (containing the slug) as an argument.
    • Uses getPostData to fetch the data for the specific blog post based on the slug.
    • Returns the data as props to the Post component.

This code dynamically generates a page for each blog post, using the slug from the URL to fetch the corresponding data.

Styling Your Blog

While this tutorial focuses on the core functionality of a Markdown-based blog, you’ll likely want to style your blog to make it visually appealing. Next.js offers several options for styling, including:

  • CSS Modules: Allows you to write CSS specific to each component, preventing style conflicts.
  • Styled Components: A popular library for writing CSS-in-JS, allowing you to style your components directly in your JavaScript code.
  • Tailwind CSS: A utility-first CSS framework that provides pre-built CSS classes for rapid styling.
  • Custom CSS: You can also use a traditional CSS file.

For this tutorial, let’s add some basic styling using CSS Modules. Create a new file called styles/global.module.css in your project and add the following:

body {<br>  font-family: sans-serif;<br>  margin: 20px;<br>}<br><br>h1 {<br>  font-size: 2em;<br>  margin-bottom: 10px;<br>}<br><br>ul {<br>  list-style: none;<br>  padding: 0;<br>}<br><br>li {<br>  margin-bottom: 10px;<br>}<br><br>a {<br>  text-decoration: none;<br>  color: blue;<br>}<br><br>a:hover {<br>  text-decoration: underline;<br>}<br>

Then, import this stylesheet into your pages/_app.tsx file (create this file if you don’t have it):

import '../styles/global.module.css';<br><br>function MyApp({ Component, pageProps }) {<br>  return <Component {...pageProps} />;<br>}<br><br>export default MyApp;

This will apply the basic styles to your blog. You can then further customize the styles by creating CSS Modules for each component. For example, to style the Home component, you could create a file styles/Home.module.css and import it into pages/index.tsx.

Common Mistakes and How to Fix Them

Here are some common mistakes and how to fix them:

  • Incorrect File Paths: Double-check the file paths, especially when importing modules and accessing Markdown files. Typographical errors can easily lead to import errors.
  • Missing Dependencies: Ensure you have installed all the necessary dependencies (gray-matter, remark, remark-html) using npm or yarn.
  • Incorrect Front Matter: Front matter must be correctly formatted at the beginning of your Markdown files, enclosed within --- lines.
  • HTML Rendering Issues: When using dangerouslySetInnerHTML, make sure your Markdown is correctly formatted and that you’re sanitizing the HTML if it comes from an untrusted source. In this case, since the content is managed by you, it’s safer.
  • Typographical Errors in Code: Carefully review your code for typos, especially in variable names, function names, and import statements. TypeScript can help catch some of these errors, but not all.
  • Incorrect File Extensions: Ensure your files have the correct extensions (.md for Markdown files, .tsx for TypeScript React components, and .css or .module.css for styling).

SEO Best Practices

To improve your blog’s search engine optimization (SEO), consider the following:

  • Use Descriptive Titles and Meta Descriptions: Ensure your blog posts have clear and concise titles and meta descriptions that accurately reflect the content.
  • Optimize Content for Keywords: Naturally incorporate relevant keywords into your blog post titles, headings, and body content.
  • Use Heading Tags (H1-H6): Structure your content with appropriate heading tags (<h1>, <h2>, etc.) to improve readability and help search engines understand the structure of your content.
  • Optimize Images: Compress your images to reduce file size and improve page load times. Use descriptive alt text for your images to help search engines understand their content.
  • Create a Sitemap: Generate a sitemap and submit it to search engines like Google and Bing to help them crawl and index your content.
  • Improve Site Speed: Optimize your website’s performance by using techniques like code splitting, image optimization, and caching. Next.js offers built-in features to help with this.
  • Use Structured Data: Implement structured data (schema markup) to provide search engines with more information about your content.
  • Ensure Mobile-Friendliness: Make sure your website is responsive and works well on all devices.

Key Takeaways

  • Next.js, TypeScript, and Markdown are a powerful combination for building a performant and maintainable blog.
  • Use gray-matter to parse front matter and remark with remark-html to convert Markdown to HTML.
  • Use getStaticProps and getStaticPaths for static site generation.
  • Organize your content in a posts directory and use dynamic routes for individual blog posts.
  • Implement SEO best practices to improve your blog’s visibility in search results.

FAQ

  1. Can I use a different Markdown parser?

    Yes, you can use any Markdown parser that suits your needs. The example in this tutorial uses remark and remark-html, but other popular options include markdown-it.

  2. How do I add images to my blog posts?

    You can add images to your Markdown files using the standard Markdown syntax: ![alt text](image-url). Ensure that the image files are accessible from your project. You can store images in your public directory or use an image hosting service.

  3. How can I deploy my blog?

    You can deploy your Next.js blog to various platforms, including Vercel, Netlify, and AWS. Vercel is particularly well-suited for Next.js projects and provides a seamless deployment experience. You can also deploy to platforms like Netlify or use a containerization tool like Docker to deploy to a cloud provider.

  4. How do I add a table of contents?

    You can add a table of contents by using a library like rehype-toc in conjunction with remark. This library will automatically generate a table of contents based on the headings in your Markdown content.

  5. How can I add code highlighting?

    You can add code highlighting using a library like rehype-prism or remark-prism (depending on your Markdown parser). These libraries will automatically highlight the code blocks in your Markdown content based on the specified language.

Building a blog with Next.js, TypeScript, and Markdown offers a streamlined and efficient approach to content creation. By leveraging the power of static site generation, you can ensure your blog is fast, SEO-friendly, and easy to maintain. From setting up your project to dynamically rendering your Markdown content, this tutorial provides a solid foundation for creating a modern and engaging blog. With the flexibility of Next.js and the simplicity of Markdown, you have a powerful combination at your fingertips, enabling you to focus on what matters most: sharing your ideas and connecting with your audience.