DEV Community

loading...

Build a markdown blog with NextJS

telmo profile image Telmo Goncalves Originally published at telmo.online Updated on ・6 min read

I've posted a Tweet about building my blog in less than an hour, and I'll be honest; it took me more time writing this post than actually putting the blog online.

I'll try to explain the steps I took.

If you don't want to follow the tutorial download the Source code.


I've decided to go ahead and create a personal page/blog for myself, and since I'm a massive fan of Zeit and Now, that meant no time wasted thinking about hosting and deployments.

I have a few projects running with using GatsbyJS, and to be honest, I love it, it's easy to use, and really powerful if you plug a third-party such as Contentful. Although this time, I wanted to try something different, and since I love hosting and deploy my projects with Zeit, why not give NextJS a try? First time using it, and let me tell you it's freaking amazing.


Let's get started

Run the following:

mkdir my-blog && cd my-blog
npm init -y && npm install react react-dom next --save

If you're wondering -y means you don't have to answer all the npm init questions

Now, in your package.json file replace scripts with:

"scripts": {
  "dev": "next",
  "build": "next build",
  "start": "next start"
}

If you go ahead and try to start the server npm run dev, it should throw an error, because NextJS is expecting to find a /pages folder.

So, let us take care of that, in the root of your project run:

mkdir pages && touch pages/index.js

Now you should be able to run npm run dev and access your application on http://localhost:3000

If everything is going as expected you should see an error similar to the following:

The default export is not a React Component in page: "/"

That's alright; keep going.


Our first view

In your pages/index.js file, paste the following code:

import React from 'react'

export default function Index() {
  return (
    <div>
      ✍️ My blog about Books
    </div>
  )
}

Check http://localhost:3000 you should see My blog about Books

Getting props

NextJS comes with a function called getInitialProps; we can pass props into our Index component.

Let us start with something simpler; at the end of your component lets put the following code:

import React from 'react'

export default function Index() {
  return (
    <div>
      ✍️ My blog about Books
    </div>
  )
}

+ Index.getInitialProps = () => {
+   return {
+     blogCategory: 'Books'
+   }
+ }

Here we're passing a blogCategory prop into our component, go ahead and change your component to look like the following:

export default function Index(props) {
  return (
    <div>
      ✍️ My blog about {props.blogCategory}
    </div>
  )
}

// ...

If you refresh the page, it should look exactly the same, although, if you change the value of blogCategory you'll see that it changes your view with the new value. Give it a try:

// ...

Index.getInitialProps = () => {
  return {
    blogCategory: 'ReactJS'
  }
}

The content of your view should now be: ✍️ My blog about ReactJS

Awesome, next!


Dynamic Routes

So, to build a blog, you want dynamic routes, according to the route we want to load a different .md file, which will contain our post data.

If you access http://localhost:3000/post/hello-world we'll want to load a file called hello-world.md, for that to happen let us follow the next steps:

First of all, NextJS is clever enough to allow us to create a [slug].js file, which is pretty awesome, let's go ahead and create that file:

mkdir pages/post

Note the folder and file needs to be created inside /pages

Now create a file inside /post called [slug].js, it's exactly like that, with the brackets.

Inside this file we'll create our post template, to display the post title, contents, etc.

Go ahead and paste the following code, we'll go over it in a minute:

import React from 'react'

export default function PostTemplate(props) {
  return (
    <div>
      Here we'll load "{props.slug}"
    </div>
  )
}

PostTemplate.getInitialProps = async (context) => {
  const { slug } = context.query

  return { slug }
}

In here we're accessing context.query to extract the slug from the URL, this is because we called our file [slug].js, let's say instead of a blog post you want to display a product page, that might contain an id, you can create a file called [id].js instead and access context.query.id.

If you access http://localhost:3000/post/hello-world you should see Here we'll load "hello-world"

Brilliant, let's keep going!


Loading Markdown Files

As a first step lets create a .md file:

mkdir content && touch content/hello-world.md

In the hello-world.md file paste the following:

---
title: "Hello World"
date: "2020-01-07"
---

This is my first blog post!

That's great; now we need to load the content of this file and pass it through props in our PostTemplate file.

Check the comments on the changed lines:

// ...

PostTemplate.getInitialProps = async (context) => {
  const { slug } = context.query
  // Import our .md file using the `slug` from the URL
  const content = await import(`../../content/${slug}.md`)

  return { slug }
}

Now that we have the data, we'll be using [gray-matter (https://www.npmjs.com/package/gray-matter) to parse our file frontmatter data.

frontmatter data is the information between --- in our .md file

To install gray-matter run:

npm install gray-matter --save

We can now parse the data and pass it to the PostTemplate props:

Don't forget to import matter

import matter from 'gray-matter'

// ...

PostTemplate.getInitialProps = async (context) => {
  // ...

  // Parse .md data through `matter`
  const data = matter(content.default)

  // Pass data to the component props
  return { ...data }
}

Awesome, now we should be able to access data in our component props. Let's try it, refresh the page... Ah, snap!

Are you getting a TypeError: expected input to be a string or buffer error?

No worries, we need to add some NextJS configuration to tell it to load .md files, this is a simple process, in the root of your project run:

touch next.config.js

Inside that new file paste the following code:

module.exports = {
  webpack: function(config) {
    config.module.rules.push({
      test: /\.md$/,
      use: 'raw-loader',
    })
    return config
  }
}

This will be using the raw-loader package, so we'll need to install that as well:

npm install raw-loader --save

Don't forget to restart your application

Now let's change our component to receive our new props:

// ...
export default function PostTemplate({ content, data }) {
  // This holds the data between `---` from the .md file
  const frontmatter = data

  return (
    <div>
      <h1>{frontmatter.title}</h1>
    </div>
  )
}

Refresh your page, you should see Hello World.

It's missing rendering the content, lets take care of that:

export default function PostTemplate({ content, data }) {
  // This holds the data between `---` from the .md file
  const frontmatter = data

  return (
    <div>
      <h1>{frontmatter.title}</h1>

      <p>{content}</p>
    </div>
  )
}

Ok, this is great, you should be able to see This is my first blog post!


Markdown Format

Now that we can render our markdown files fine, let's add some formatting to our post file, go ahead and change hello-world.md:

---
title: "Hello World"
date: "2020-01-07"
---

### Step 1

- Install dependencies
- Run locally
- Deploy to Zeit

Hmmm, the format is not working like expected, it's just raw text.

Let's take care of that, we'll be using react-markdown to handle markdown formatting:

npm install react-markdown --save

Now lets update our PostTemplate component:

import React from 'react'
import matter from 'gray-matter'
import ReactMarkdown from 'react-markdown'

export default function PostTemplate({ content, data }) {
  // This holds the data between `---` from the .md file
  const frontmatter = data

  return (
    <div>
      <h1>{frontmatter.title}</h1>

      <ReactMarkdown source={content} />
    </div>
  )
}

That's it; we are done here! You can download the final code here.


If you liked this post, I would really appreciate if you could share it with your network and follow me on Twitter 👏

Discussion

pic
Editor guide
Collapse
kozakrisz profile image
Krisztian Kecskes

Thanks! This article is very helpful!

Collapse
telmo profile image
Telmo Goncalves Author

Thanks Krisztian 🎉

Collapse
kozakrisz profile image
Krisztian Kecskes

Do you have any idea how to implement MDX support into this logic?

Thread Thread
telmo profile image
Telmo Goncalves Author

I do not, but I can explore it.

Collapse
imranib profile image
Imran Irshad

That is very helpful. Thanks

Collapse
kerrytokyo profile image
Kerry

Thank you for writing this article!
It was helpful.

Collapse
telmo profile image
Telmo Goncalves Author

Thank you Kerry 😃

Collapse
en0ma profile image
Lawrence Enehizena

Hi Telmo, I think you have a typo in the code here - const content = import(../../content/${slug}.md) , it should be const content = await import(../../content/${slug}.md)

Collapse
telmo profile image
Telmo Goncalves Author

Hey Lawrence, you're right. Thanks for the heads up, will change it 👍

Collapse
mohansingh profile image
Mohansingh Omprakash

So i have a doubt , Suppose if i have a count of 500 posts in my website ,should i need to create 500 markdown files for that?
Please help in an efficient approach

Collapse
josiasaurel profile image
Josias Aurel

Awesome tutorial. This very helpful

Collapse
zhangkaiyulw profile image
Zhang Kai Yu

Thanks this is very helpful!