DEV Community

Cover image for How to create a Blog using Next.js and Markdown.
Anurag Gharat
Anurag Gharat

Posted on

How to create a Blog using Next.js and Markdown.

Hello Devs! Today we will see How to create your Blog using Next.js and Markdown!

Markdown

Markdown is a simple syntax that is used to format text into Headers, Lists, Italic, Bold etc. Markdown makes it easy to format texts for you. The syntax of Markdown is simpler to HTML. Markdown files end with .md extension. Readme file which we use in GitHub uses Markdown.

Here’s a example on syntax of Markdown

# This is Heading1 <h1>
###### This is Heading6 <h6>
*Bullet Point
1.Numbered Point
_Italic text_
Enter fullscreen mode Exit fullscreen mode

So by now you would have understood what Markdown does. In our Blogging website we will create Posts in Markdown syntax and display them on our website.

Getting Started

Lets create a fresh Next.js application using npx.

npx create-next-app myblog
cd myblog 

Enter fullscreen mode Exit fullscreen mode

We created a new Next.js project by the name “myblog”. Now open up this folder in your favorite code editor. I’ll use VS code here.

I am using Tailwind CSS for styling my webpages. If you prefer Bootstrap or writing your CSS you can go ahead with your preference. If you want to know how to add Tailwind CSS to your Next.js application read my article here.

https://dev.to/anuraggharat/how-to-add-tailwind-css-in-next-js-3epn.

So lets understand what we will be creating.

We are creating a simple website which will have two pages - Home page and Blog page. The Home page will have the list of all Blogs and we will generate individual Static Blog pages for each blog.

So lets start!

Setting up the Project.

Create two folders named components and posts in the root directory of your project. Posts folder will house all our Posts which will be written in Markdown while Components folder will have all our components which we will use.

Now create a folder named blogs inside your pages folder. Inside that blogs folder add a page with the name [id].js. We will be using dynamic routes to showcase each post. If you want to learn more about dynamic routes, I would suggest you to read this. https://nextjs.org/docs/routing/dynamic-routes

To summarize dynamic routes.

Next.js allows you to create dynamic routes for the files inside square brackets.

So since we have [id].js in posts folder, the page will be called when we call URL localhost:3000/blogs/1243 . Since the route is dynamic anything after /blogs/ will call [id].js.
Further in this tutorial we will create static page for each posts using getStaticProps() and getStaticPaths().

Moving on, clear the existing code from index.js file. Once done your index.js file should look like this.

export default function Home() {
  return (
    <div>

    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

So after doing all this you would be left with something like this.

Folder Structure

Creating Dummy Markdown posts

We need at least 2 posts to show on our website and test it. So lets quicky create them.

Copy the following dummy code and create two files in posts directory. Name the files by the title name since we will be using the file name as ‘url parameter’. For example localhost:3000/blog/first-post

I am creating two files with same content but only changing the title so that I can distinguish them.

Name of my files is first-blog and second-blog. Remember to use .md extension.

---
title: 'My First Blog of 2022'
metaTitle: 'My First blog of 2022'
metaDesc: 'How to make a blogging website using Next.js, Markdown and style it using TailwindCSS.'
socialImage: images/pic1.jpg
date: '2022-02-02'
tags:
  - nextjs
  - personal
  - health
  - work
---
# The main content
# One morning, when Gregor Samsa woke from troubled dreams.
One morning, when Gregor Samsa woke from troubled dreams, he found himself *transformed* in his bed into a horrible  [vermin](http://en.wikipedia.org/wiki/Vermin "Wikipedia Vermin"). He lay on his armour-like back, and if he lifted his head a little he could see his brown belly, slightly domed and divided by arches into stiff sections. The bedding was hardly able to cover **strong** it and seemed ready to slide off any moment. His many legs, pitifully thin compared with the size of the rest of him, link waved abouthelplessly as he looked. <cite>“What's happened to me?”</cite> he thought. It wasn't a dream. His room, a proper human room although a little too small, lay peacefully between its four familiar walls.</p>

## The bedding was hardly able to cover it.
Enter fullscreen mode Exit fullscreen mode
---
title: 'My Second Blog of 2022'
metaTitle: 'My Second blog of 2022'
metaDesc: 'How to make a blogging website using Next.js, Markdown and style it using TailwindCSS.'
socialImage: images/pic2.jpg
date: '2022-02-02'
tags:
  - nextjs
  - personal
  - health
  - work
---

# The main content

# One morning, when Gregor Samsa woke from troubled dreams.
One morning, when Gregor Samsa woke from troubled dreams, he found himself *transformed* in his bed into a horrible  [vermin](http://en.wikipedia.org/wiki/Vermin "Wikipedia Vermin"). He lay on his armour-like back, and if he lifted his head a little he could see his brown belly, slightly domed and divided by arches into stiff sections. The bedding was hardly able to cover **strong** it and seemed ready to slide off any moment. His many legs, pitifully thin compared with the size of the rest of him, link waved abouthelplessly as he looked. <cite>“What's happened to me?”</cite> he thought. It wasn't a dream. His room, a proper human room although a little too small, lay peacefully between its four familiar walls.</p>

## The bedding was hardly able to cover it.
Enter fullscreen mode Exit fullscreen mode

I have added lot more content below it. You can skip it as it doesn’t make any difference.

The top part between “- - -” and “- - -” is called as Frontmatter. It is basically the meta data and will not be rendered.

Creating the Website and Styling it using Tailwind CSS

For the sake of the blog we are going to make just a basic not so fancy website with minimal styles. But you are free to use your creativity. We will create a home page which will list all the blogs and a blog page which will show individual blog contents. So lets start

Create a reusable layout component in components folder and import it in _app.js file.

Layout.js

import Link from "next/link";

function Layout({children}) {
  return (
    <div className="w-full min-h-screen ">
      <div className="flex flex-row h-16 justify-around align-middle">
        <h1 className="my-auto text-2xl font-mono">Simple Blog</h1>
        <Link href={`/`}>
          <a className="my-auto">Github Code</a>
        </Link>
      </div>
      <div className="container md:w-3/5 w-5/6 mx-auto mt-16">
        {children}
      </div>
    </div>
  );
}

export default Layout;
Enter fullscreen mode Exit fullscreen mode

We have created a simple layout where we have kept a header and we will be rendering the children below it.

Import this Layout.js component in _app.js file.

_app.js

import Layout from '../components/Layout'
import '../styles/globals.css'

function MyApp({ Component, pageProps }) {
  return (
    <Layout>
    <Component {...pageProps} />
  </Layout>
  ) 

}

export default MyApp
Enter fullscreen mode Exit fullscreen mode

Now, we will create a reusable Blog card component to show the blogs on the index.js page. So lets create a component Blogcard.js in components folder.

Blogcard.js

import Link from "next/link";

function Blogcard() {
  return (
    <div className="container w-100 mx-auto mb-16">
      <img
        className="w-3/4 rounded-lg mx-auto drop-shadow-lg"
        src="https://images.pexels.com/photos/675764/pexels-photo-675764.jpeg?auto=compress&cs=tinysrgb&dpr=2&w=500"
      />
      <Link href={'/'}>
        <h1 className="text-4xl font-semibold mt-4">
          Here is my first blog of the website
        </h1>
      </Link>
      <p className="text-gray-600 text-sm">2 Feb 2022</p>
      <p>
        This is just a static blog written to test the component structure.This
        is just a static blog written to test the component structure. is just a
        static blog written to test the component structure.
      </p>
    </div>
  );
}

export default Blogcard;
Enter fullscreen mode Exit fullscreen mode

Currently for styling purpose I have added dummy static data and images here. Once we import the markdown files we will add posts data dynamically. For now to test how our blog will appear, add some dummy data.

Import Blog card in index.js file

index.js

import Blogcard from "../components/Blogcard";

export default function Home() {
  return (
    <div>
      <Blogcard />
      <Blogcard />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

So now we have created the overall structure of our websites. Now we have to render the posts content on the page.

Loading the Posts on our Homepage

Go ahead and create a images folder in public folder. We earlier used social images in our markdown, this is the folder where we will store all the images. I am naming the images as “pic1” and “pic2” because that’s what I have named them in markdown files. I have 2 beautiful images imported from Pexels.

In order to extract the content we would need a package named gray-matter. So let’s install it using “npm”.

npm install gray-matter
Enter fullscreen mode Exit fullscreen mode

What gray-matter does?

“Parse front-matter from a string or file. Fast, reliable and easy to use. Parses YAML front matter by default, but also has support for YAML, JSON, TOML or Coffee Front-Matter, with options to set custom delimiters. Used by metalsmith, assemble, verb and many other projects.”

Now open your index.js file. We will import the markdown content and parse it using gray-matter here.

Add these import statement in index.js.

import fs from 'fs'
//FS to read files 
import matter from "gray-matter";
Enter fullscreen mode Exit fullscreen mode

Now we will use getStaticProps() method which is a Data Fetching method which runs only during build time and passes props to the page.

export async function getStaticProps(){
  // Getting all our posts at build time

  // Get all the posts from posts folder
  const files = fs.readdirSync("posts");

  // Loop over each post to extract the frontmatter which we need
  const posts = files.map((file) => {
    // getting the slug here which we will need as a URL query parameter
    const slug = file.replace(".md", "");
    // Reading the contents of the file
    const filecontent = fs.readFileSync(`posts/${file}`, "utf-8");
    const parsedContent = matter(filecontent);
    //The parsed content contains data and content we only need the data which is the frontmatter
    const {data} = parsedContent
    return {
      slug,
      data,
    };
  });

  return {
    props:{
      posts
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

If you want to learn more about Data Fetching in Next.js, make sure you read my other blog.

Here we are getting the slugs from the name of the file. Slugs will serve as the Url Parameter for loading each post. Then we are reading the data from each file and parsing it using gray-matter. We de-structure the data from the content since we only want the data which is the frontmatter(meta data) from the posts right now. Then we are collecting all the data in posts array and returning it as props to the index.js page. Use console log statements to understand better which line returns what.

Lets now collect the data in index.js page and pass it to the ‘Blogcard’ component.

import Blogcard from "../components/Blogcard";
import fs from 'fs'
import matter from "gray-matter";

export default function Home(props) {
  const {posts} = props
  return (
    <div>

      {posts.map((post,index)=>(
        <Blogcard key={index} post={post} />
      ))}

    </div>
  );
}
export async function getStaticProps(){
  // Getting all our posts at build time

  // Get all the posts from posts folder
  const files = fs.readdirSync("posts");

  // Loop over each post to extract the frontmatter which we need
  const posts = files.map((file) => {
    // getting the slug here which we will need as a URL query parameter
    const slug = file.replace(".md", "");
    // Reading the contents of the file
    const filecontent = fs.readFileSync(`posts/${file}`, "utf-8");
    const parsedContent = matter(filecontent);
    //The parsed content contains data and content we only need the data which is the frontmatter
    const {data} = parsedContent
    return {
      slug,
      data,
    };
  });

  return {
    props:{
      posts
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

We are receiving the data from getStaticProps() function and extracting posts array from it. We are then mapping the array and passing each item from the array to “Blogcard” component.

In “Blogcard” we are extracting the data and rendering it.

Blogcard.js

import Link from "next/link";

function Blogcard({post}) {
    console.log(post)
  return (
    <div className="container w-100 mx-auto mb-16">
      <img
        className="w-3/4 rounded-lg mx-auto drop-shadow-lg"
        src={post.data.socialImage}
      />
      <Link href={`blog/${post.slug}`}>
        <h1 className="text-4xl font-semibold mt-4">{post.data.metaTitle}</h1>
      </Link>
      <p className="text-gray-600 text-sm">{post.data.date}</p>
      <p>{post.data.metaDesc}</p>
    </div>
  );
}

export default Blogcard;
Enter fullscreen mode Exit fullscreen mode

So if everything goes well you will be getting your two posts on your home page. If you get any error please check the data that you are passing has reached the component or not. Use console logs to test every code segment.

Output

Output

Creating Individual pages for Blogs

Now lets create individual pages for each blog. We will create one page which will run for each post and using getStaticProps() and getStaticPaths() we will generate individual static pages for each post.

This is what my [id].js looks like. I renamed the function Blog.

export default function Blog() {
  return <div></div>;
}
Enter fullscreen mode Exit fullscreen mode

So open our [id].js page in blog folder and add the following code.

Import gray-matter and filesystem(fs)

import fs from 'fs';
import matter from 'gray-matter';
Enter fullscreen mode Exit fullscreen mode

getStaticPaths()

export async function getStaticPaths() {
  // Get all the paths from slugs or file names
  const files = fs.readdirSync("posts");
  const paths = files.map((files) => ({
    params: {
      id: files.replace(".md", ""),
    },
  }));
  console.log("paths",paths)
  return {
    paths,
    fallback:false
  }
}
Enter fullscreen mode Exit fullscreen mode

So in this function we are generating an array of all the valid paths. These paths are slug names which will load blog posts for that name. And we then return these paths along with fallback as false. Fallback will show 404 page for wrong URL’s. Read more about getStaticPaths() here

getStaticPaths()

export async function getStaticProps({params:{id}}){
    const fileName = fs.readFileSync(`posts/${id}.md`, "utf-8");
    const { data: frontmatter, content } = matter(fileName);
    return {
      props: {
        frontmatter,
        content,
      },
    };
}
Enter fullscreen mode Exit fullscreen mode

This function is similar to the function we wrote in index.js page for fetching list of blogs. The only difference is here we are finding one post by taking in the id passed in the URL and returning the entire post to the page. Lets put some console log and check it we are receiving the content.

export default function Blog({ frontmatter ,content}) {
  console.log(frontmatter)
  console.log(content);

  return <div></div>;
}
Enter fullscreen mode Exit fullscreen mode

Hurray! We got the content! But wait the content that we fetched is in markdown format we cant directly show it here. For that lets install markdown-it.

npm i markdown-it
Enter fullscreen mode Exit fullscreen mode

This package will convert markdown into HTML code which we can then render on our webpage.

Add some classes and render the content as below:


import fs from "fs";
import matter from "gray-matter";
import md from 'markdown-it'

export default function Blog({ frontmatter ,content}) {

  return (
    <div>
      <img src={`/${frontmatter.socialImage}`} className="w-3/4 mx-auto" />
      <div className="">
        <h1 className="text-3xl">{frontmatter.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: md().render(content) }}></div>
      </div>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

We are using “dangerouslySetInnerHTML” to apply our HTML content in our webpage.

Now your first impression is the content is rendered but something looks off. Yes the typography isn’t right yet. Don’t worry Tailwind is here to save us. Install this tailwind typography plugin.

npm install -D @tailwindcss/typography
Enter fullscreen mode Exit fullscreen mode

Using this plugin you can give a “className” as “prose” to a div and it will style everything inside that div in a proper way.

Once installed add it to the tailwind.config.js file

module.exports = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [require("@tailwindcss/typography")],
};
Enter fullscreen mode Exit fullscreen mode

Now give className as prose to the outermost div of the rendered content.

So finally your [id].js file should something like this.

import fs from "fs";
import matter from "gray-matter";
import md from 'markdown-it'

export default function Blog({ frontmatter ,content}) {

  return (
    <div className="w-100">
      <img src={`/${frontmatter.socialImage}`} className="w-3/4 mx-auto" />
      <div className="prose w-3/4  mx-auto">
        <h1 className="text-3xl">{frontmatter.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: md().render(content) }}></div>
      </div>
    </div>
  );
}

export async function getStaticPaths() {
  // Get all the paths from slugs or file names
  const files = fs.readdirSync("posts");
  const paths = files.map((files) => ({
    params: {
      id: files.replace(".md", ""),
    },
  }));
  console.log("paths",paths)
  return {
    paths,
    fallback:false
  }
}

export async function getStaticProps({params:{id}}){
    const fileName = fs.readFileSync(`posts/${id}.md`, "utf-8");
    const { data: frontmatter, content } = matter(fileName);
    return {
      props: {
        frontmatter,
        content,
      },
    };
}
Enter fullscreen mode Exit fullscreen mode

And yes you have finally created your personal Blog using Next.js and Markdown. You can use your own creativity and style it.

Final Output

Blog on my personal portfolio is also built using the same method. Check it here.

Portfolio

I am linking the link for GitHub repository here for you to reference! Thank you for reading. Follow me on twitter, I regularly post content about Web Development and Programming. Happy Coding!

GitHub - anuraggharat/Simple-Blog:

Top comments (1)

Collapse
 
kr1st1nagr03g3r profile image
Kristina Groeger • Edited

Curious why you're using <img> tags when this is a Next.js project? It should be

<Image>
Enter fullscreen mode Exit fullscreen mode

instead. <img> throws errors.

As well, in Blogcard.js, you will have to provide props for image alt and sizes as per Next.js reference.

So, I'd add:

import Image from 'next/image'
import Link from 'next/link'

function Blogcard({ post }) {
console.log(post)
return (
<div className="">
<Image
width={1200}
height={300}
alt={post.data.alt}
src={post.data.socialImage}
/>
<Link href={
blog/${post.slug}}>
<h1 className="text-4xl font-semibold mt-4">{post.data.metaTitle}</h1>
</Link>
<p className="text-gray-600 text-sm">{post.data.date}</p>
<p>{post.data.metaDesc}</p>
</div>
)
}
export default Blogcard

Then, to your blog posts, add "alt" tags to make it accessible:

title: 'My First Blog of 2022'
metaTitle: 'My First blog of 2022'
metaDesc: 'How to make a blogging website using Next.js, Markdown and style it using TailwindCSS.'
socialImage: images/pic1.jpg
alt: "a couple of cute penguins in the park"
date: '2022-02-02'

nextjs.org/docs/api-reference/next...