Go back to blogs

How to build a SEO ready Next.js Blog using Markdown

Nov 25, 2023


I have been wanting to build a blog for awhile, I have heard of the wonders of blogs for site discoverability with Search Engine Optimization. I always thought it would be hard to build, or that I would need to use something like Wordpress. One weekend I finally watched a few videos, read some articles and worked with ChatGPT to learn, it wasn't that bad. So in this article, I will show you all the steps I took to build this blog, so that you can have a working blog in no time.

Setting Up Your Next.js Blog

Step 1: Setup the Folder Structure for Blog Posts

Organize your blog posts in a specific directory within your project. A common approach is to use a posts directory within the public or a dedicated content directory.

- /pages
  - /blog
    - index.tsx (or .js)
    - [slug].tsx (or .js)
- /posts
  - my-first-post.md
  - my-second-post.md
  - ...

Each Markdown file in the posts directory represents a blog post. The file name can be used as the URL slug for the blog post.

Step 2: Reading Markdown Files

To read and parse Markdown files, we will use the libraries remark and gray-matter. These libraries allow you to parse the Markdown content and also extract metadata (like title, date, etc.) defined at the top of each Markdown file.

Install the dependencies

npm install gray-matter remark remark-html

Step 3: Writing Your Blog List Page

in pages/blog/index.tsx

import Head from "next/head";
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import Link from 'next/link';

export default function BlogIndex({ posts }) {
  return (
    <div>
      <Head>
        <title>My Blog</title>
        <meta name="description" content="Where I write about my thoughts and ideas." />
      </Head>
      <h1>Blog</h1>
      {/* TODO: You can add other info and styling to the blog list here */}
      <ul>
        {posts.map(post => (
          <li key={post.slug}>
            <Link href={`/blog/${post.slug}`}>
              <a>{post.frontmatter.title}</a>
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

export async function getStaticProps() {
  // Get files from the posts directory
  const files = fs.readdirSync(path.join('posts'));

  // Get slug and frontmatter from posts
  const posts = files.map(filename => {
    const slug = filename.replace('.md', '');
    const markdownWithMeta = fs.readFileSync(
      path.join("posts", filename),
      "utf-8"
    );
    const { data: frontmatter } = matter(markdownWithMeta);

    return {
      slug,
      frontmatter,
    };
  });

  // Sort posts by date
  posts.sort(
    (a, b) =>
      new Date(b.frontmatter.date).getTime() -
      new Date(a.frontmatter.date).getTime()
  );

  return {
    props: {
      posts,
    },
  };
}

Step 4: Processing the Individual Blog Posts

in pages/blog/[slug].tsx

This page will display the content of an individual blog post.

import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { remark } from 'remark';
import html from 'remark-html';
import Head from 'next/head';
// Create your styles file if you haven't
import styles from "../../styles/blog.module.css";

export default function BlogPost({ frontmatter, content, slug }) {
  return (
    <div>
      <Head>
        <title>{frontmatter.title}</title>
        <meta name="description" content={frontmatter.description} />
      </Head>
      <article>
        <h1>{frontmatter.title}</h1>
        {/* TODO: You can add other info and styling to the top of your blog here */}
        <div
            className={styles.markdown}
            dangerouslySetInnerHTML={{ __html: props.content }}
        />
      </article>
    </div>
  );
}

export async function getStaticPaths() {
  // Get filenames from the posts directory
  const files = fs.readdirSync(path.join('posts'));
  const paths = files.map(filename => ({
    params: {
      slug: filename.replace('.md', ''),
    },
  }));

  return {
    paths,
    fallback: false,
  };
}

export async function getStaticProps({ params: { slug } }) {
  const markdownWithMeta = fs.readFileSync(
    path.join("posts", slug + ".md"),
    "utf-8"
  );
  const { data: frontmatter, content } = matter(markdownWithMeta);

  // Convert markdown into HTML string
  const processedContent = await remark().use(html).process(content);
  const contentHtml = processedContent.toString();

  return {
    props: {
      frontmatter,
      content: contentHtml,
      slug,
    },
  };
}

Step 5: Create Your First Blog Post

Each Markdown file should have metadata at the top followed by the content of the post.

---
title: 'My First Post'
date: '2023-11-26'
description: 'This is my first post!'
---

This is the content of my first post.

Step 6: Create Basic Styling for Your Markdown

You may have noticed we are importing styles from blog.module.css in . Lets create that file.

/* Styling for the markdown Blog Posts */
/* Use of the .markdown prefix is required
to keep the classes in scope of the blog posts */

.markdown * {
  color: #222;
}

.markdown h1 {
  font-size: 2em;
  margin-top: 2rem;
  margin-bottom: 1.5rem;
}

.markdown h2 {
  font-size: 1.75em;
  margin-top: 1.5rem;
  margin-bottom: 1rem;
}

.markdown h3 {
  font-size: 1.5em;
  margin-top: 1rem;
  margin-bottom: .75rem;
}

.markdown h4 {
  font-size: 1.25em;
  margin-top: .75rem;
  margin-bottom: .5rem;
}

.markdown h5 {
  font-size: 1.1em;
  margin-top: .5rem;
  margin-bottom: .25rem;
}

.markdown p {
  font-size: 1em;
  line-height: 1.6;
  margin: .5rem 0;
}

.markdown hr {
    border-color: #ddd;
    margin: 1.75rem 0;
}

.markdown ul {
    list-style-type: disc;
    padding-left: 1.5em;
}

.markdown ol {
    list-style-type: decimal;
    padding-left: 1.5em;
}

.markdown li {
    margin-bottom: 0.5em;
}

.markdown blockquote {
  border-left: 4px solid #ccc;
  margin-left: 0;
  padding-left: 1em;
  color: #444;
}

.markdown a {
    color: #2c25aa;
    text-decoration: underline;
}

.markdown code {
  background-color: #DDD;
  padding: 0 1px;
}

.markdown pre {
  padding: .4rem .4rem .4rem .6rem;
  border-radius: 5px;
  background-color: #EEE;
  max-width: 100%;
  overflow-x: auto;
}

.markdown pre > code {
  background-color: unset;
}

Since your Markdown content is rendered into HTML and set via dangerouslySetInnerHTML, the styles you defined in your CSS files will be applied to these elements.

Modify the styles to your taste and add more as you need them.

Setting Up SEO

Create a robots.txt in the public Folder

robots.txt is used to instruct web crawlers about the pages you want or don't want to be crawled and indexed. It typically contains rules for search engine bots, like disallowing access to certain directories or specifying the location of the sitemap. An example would be:

User-agent: *
Allow: /admin/
Sitemap: https://www.yourdomain.com/sitemap.xml

After deploying your site test it by visiting https://www.yourdomain.com/robots.txt.

Dynamically Generate your Sitemap

A sitemap is an XML file that lists all important pages of your website, making it easier for search engines to crawl them.

Edit with any other routes/pages you have so they are crawled by search engines.

Example Script scripts/generate-sitemap.js

const fs = require('fs');
const path = require('path');

// Function to get slugs from markdown files in the posts/ directory
function getBlogPostSlugs() {
  const postsDir = path.join(__dirname, '../posts');
  return fs.readdirSync(postsDir)
    .filter(filename => filename.endsWith('.md'))
    .map(filename => filename.replace('.md', ''));
}

async function generateSitemap() {
  const posts = getBlogPostSlugs();

  const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    <url>
        <loc>https://www.yourdomain.com/</loc>
    </url>
    <url>
        <loc>https://www.yourdomain.com/blog</loc>
    </url>
    ${posts.map(slug => `
    <url>
        <loc>${`https://www.yourdomain.com/blog/${slug}`}</loc>
    </url>
    `).join('')}
</urlset>
  `;

  fs.writeFileSync(path.join(__dirname, '../public', 'sitemap.xml'), sitemap);
}

generateSitemap();
  • The getBlogPostSlugs function reads the filenames from the posts/ directory, filters out non-markdown files, and maps the filenames to slugs by removing the .md extension.

  • The generateSitemap function creates a sitemap string, including URLs for your home, art, and blog list pages, along with dynamically generated URLs for each blog post.

  • Finally, the sitemap is written to public/sitemap.xml.

Integrate the script with your build process in package.json

"scripts": {
  "build": "node scripts/generate-sitemap.js && next build"
}

Google Search Console

After deploying your site with the sitemap, use Google Search Console to submit your sitemap URL. Google will report if there are any issues found.

Also if it has been some time and your page is not yet indexed by Google when you search, you can manually request indexing for that page or route.


Congrats! You now have a blog post and you should be able to search your blogs once Google Indexes your site with the new sitemap.xml.

If you enjoyed this tutorial, I would appreciate if you bookmarked My Blog List. Thanks!