How To Create a Simple Blog with Next.js and Markdown

Lets create a simple blog listing and navigation using Next.js and Markdown

Posted by Nuno Alves on Oct 22 2020

Article in english

10 min · No views · No likes

markdown
nextjs
reactjs
javascript

Create Simple Blog with Next.js and Markdown

photo by Jess Bailey on Unsplash

In this short tutorial I'll walk you through how you can create an easy and quick (very simple) blog website for publishing your articles using Next.js and Markdown.

What is Next.js

Next.js is a production ready open source React framework that allows quick generation of both static and dynamic websites using JAMstack. It provides functionalities like Pre-rendering, both SSG (Static Site Generation) and SSR (Server Side Rendering), hybrid SSG & SSR, Client-side routing, API routes, TypeScript support to name a few.

What is Markdown

Markdown is a plain text formatting syntax, used to add formatting capabilities to plain text documents.

Creating Next.js project

To create a Next.js application, open the terminal and just run the following command:

npx create-next-app nextjs-markdown-blog

After, you should have a folder called nextjs-markdown-blog.

Execute the following commands

cd nextjs-markdown-blog
yarn dev
# or
npm run dev

And development server starts on port 3000 by default. Open http://localhost:3000 from your browser and you should see the following:

Next.js Development Server Running

If you see the previous window, the starter template page, it means everything went smoothly and we are ready to proceed!

Next.js Application folder structure

Folder Structure

You should have the above folder structure. I'll not enter into details about Next.js folder structure and routing since it is not on the scope of this tutorial, but you can check the official documentation here.

To keep the simplicity of this article and focus on what is presented, you can delete folders styles and api under pages.

Also, delete the first line of _app.js:

1
import '../styles/globals.css';
2

That's should do it for now.

Starting Blog implementation

Let's start by creating a site.config.json file in the root folder, so we have a unique place for website global configuration without hardcoding values on each file. On it, place the following code:

1
{
2
"title": "My Blog Website",
3
"description": "Simple Next.js Markdown Blog Website"
4
}
5

Now, go to index.js and type the following code, replacing the existing one:

1
const Index = () => {
2
return <h1>My Blog Website</h1>;
3
};
4
5
export default Index;
6

You should now see on http://localhost:3000:

First Screen

Let's now add a little bit of page customization using layouts, but first, let me briefly explain how Data Fetching works on Next.js:

As stated in the beginning of the article, we can have two types of pre-rendering, Static Generation or Server Side Rendering. Basically we have three methods for fetching data for pre-rendering:

  • Static Generation using
    • getStaticProps: for fetching data at build time
    • getStaticPaths: for defining paths / routes to pre-render based on data
  • Server Side Rendering using
    • getServerSideProps: for fetching data on each request

Let's then, using Static Generation strategy for our blog, inject previously defined site configurations into the page. Add the following code to index.js

1
const Index = ({ title, description, ...props }) => {
2
return (
3
<>
4
<h1>{title}</h1>
5
<p>{description}</p>
6
</>
7
);
8
};
9
10
export const getStaticProps = async () => {
11
const siteConfig = await import('../../site.config.json');
12
13
return {
14
props: {
15
title: siteConfig.title,
16
description: siteConfig.description
17
}
18
};
19
};
20
21
export default Index;
22

This should be what we see when we navigate to our index page now:

Second Screen

Adding Blog template components

We will create a new folder in the root of the project called components. Inside we will create 3 files Footer.js, Header.js and Layout.js.

1
const Footer = () => {
2
return <footer>Copyright 2020</footer>;
3
};
4
5
export default Footer;
6
1
import Link from 'next/link';
2
3
const Header = () => {
4
return (
5
<header className="content">
6
<nav className="navigation">
7
<Link href="/" passHref>
8
<a>My Blog</a>
9
</Link>
10
<Link href="/about" passHref>
11
<a>About</a>
12
</Link>
13
</nav>
14
</header>
15
);
16
};
17
18
export default Header;
19
1
import Head from 'next/head';
2
import Footer from './Footer';
3
import Header from './Header';
4
5
const Layout = ({ children, title, ...props }) => {
6
return (
7
<>
8
<Head>
9
<title>{title}</title>
10
<meta title={title} />
11
</Head>
12
<section className="content-wrapper">
13
<Header />
14
<div className="content">{children}</div>
15
</section>
16
17
<Footer />
18
</>
19
);
20
};
21
22
export default Layout;
23

Now, adapt index.js and create a new page about.js:

1
import Layout from '../components/Layout';
2
3
const Index = ({ title, description, ...props }) => {
4
return (
5
<Layout title={title}>
6
<h1>{title}</h1>
7
<p>{description}</p>
8
<main>
9
<p>Blog Posts here!</p>
10
</main>
11
</Layout>
12
);
13
};
14
15
export const getStaticProps = async () => {
16
const siteConfig = await import('../site.config.json');
17
18
return {
19
props: {
20
title: siteConfig.title,
21
description: siteConfig.description
22
}
23
};
24
};
25
26
export default Index;
27
1
import Layout from '../components/Layout';
2
3
const About = ({ title, description, ...props }) => {
4
return (
5
<Layout title={`${title} - About`}>
6
<h1>{title}</h1>
7
<p>{description}</p>
8
<p>This is my About Page!</p>
9
</Layout>
10
);
11
};
12
13
export const getStaticProps = async () => {
14
const siteConfig = await import('../site.config.json');
15
16
return {
17
props: {
18
title: siteConfig.title,
19
description: siteConfig.description
20
}
21
};
22
};
23
24
export default About;
25

This is how our site looks up to now:

Index Screen

About Screen

Adding Posts Dynamic Routing and Markdown processing

For this step we will need to install the following additional packages:

  • gray-matter: parse front-matter from string or file
  • react-markdown: for rendering markdown in react
  • raw-loader: webpack loader to allow importing files as a String.

Run the following commands:

yarn add gray-matter react-markdown raw-loader
or
npm install gray-matter react-markdown raw-loader

Create a next.config.js file at root, paste the following and restart the server afterward:

1
module.exports = {
2
webpack: function (config) {
3
config.module.rules.push({
4
test: /\.md$/,
5
use: "raw-loader",
6
});
7
return config;
8
},
9
};
10

Now, let's create a dynamic route! For this, we will create a post folder under pages and a file [slug].js under newly created folder post. Also create another folder named posts at root, where markdown blog posts will be added. Next.js let us add brackets to a page to create these dynamic routes (slugs) and make possible to access what page was called using this slug inside the file.

Add the following to [slug].js:

1
import Link from "next/link";
2
import Layout from "../../components/Layout";
3
import matter from "gray-matter";
4
import ReactMarkdown from "react-markdown";
5
6
const Post = ({ title, frontmatter, markdownContent }) => {
7
if (!frontmatter) {
8
return <></>;
9
}
10
11
return (
12
<Layout title={`${title} - ${frontmatter.title}`}>
13
<Link href='/' passHref>
14
<a>Back</a>
15
</Link>
16
<article>
17
<h1>{frontmatter.title}</h1>
18
<p>{frontmatter.author}</p>
19
<p>
20
<ReactMarkdown source={markdownContent} />
21
</p>
22
</article>
23
</Layout>
24
);
25
};
26
27
export const getStaticProps = async ({ ...ctx }) => {
28
const { slug } = ctx.params;
29
30
const siteConfig = await import("../../site.config.json");
31
const content = await import(`../../posts/${slug}.md`);
32
const data = matter(content.default);
33
34
return {
35
props: {
36
title: siteConfig.title,
37
frontmatter: data.data,
38
markdownContent: data.content,
39
},
40
};
41
};
42
43
export const getStaticPaths = async () => {
44
const slugs = ((ctx) => {
45
const keys = ctx.keys();
46
const data = keys.map((key, index) => {
47
return key.replace(/^.*[\\\/]/, "").slice(0, -3);
48
});
49
return data;
50
})(require.context("../../posts", true, /\.md$/));
51
52
const paths = slugs.map((slug) => `/post/${slug}`);
53
54
return {
55
paths,
56
fallback: false,
57
};
58
};
59
60
export default Post;
61
62

In getStaticProps we retrieve information from site.config.js as we've done previously, but now, we also return metadata - using gray-matter to parse it - and markdown data from our blog post markdown file.

In getStaticPaths we define the list of paths that will be rendered at build time. We iterate over all files on posts folder, parse files names to define respective slugs and return a path list based on those ones. We also set fallback to false so 404 is returned in case some page is not under this list.

Let's finally create a markdown blog post!
Create a file under posts folder and name it whatever you like, for example myfirstpost.md. Use markdown and add something like the following:

1
---
2
title: My first post
3
author: Nuno Alves
4
---
5
6
# First Heading
7
8
Pariatur nostrud fugiat do deserunt occaecat excepteur. Consequat consectetur consequat exercitation pariatur ex ex proident ullamco ex velit officia amet laborum exercitation. Nostrud commodo eu duis sint.
9
10
Elit irure adipisicing officia enim ea eiusmod. Ad officia reprehenderit aute fugiat eiusmod ipsum ad Lorem pariatur commodo mollit esse proident. Velit magna fugiat ad veniam pariatur incididunt reprehenderit voluptate veniam aliquip.
11
12
## Some List
13
14
- Item 1
15
- Item 2
16
- Item 3
17

If you now navigate to our post page you should see

Post Screen

Let's now complete ou simple blog by creating a new component PostList.js under components folder for listing our blog posts...

1
import Link from "next/link";
2
3
const PostList = ({ posts }) => {
4
if (posts === "undefined") {
5
return null;
6
}
7
8
return (
9
<div>
10
{!posts && <div>No posts found!</div>}
11
<ul>
12
{posts &&
13
posts.map((post) => (
14
<li key={post.slug}>
15
<Link href={{ pathname: `/post/${post.slug}` }} passHref>
16
<a>{post.frontmatter.title}</a>
17
</Link>
18
</li>
19
))}
20
</ul>
21
</div>
22
);
23
};
24
25
export default PostList;
26

...and updating index.js to display it.

1
import matter from "gray-matter";
2
import Layout from "../components/Layout";
3
import PostList from "../components/PostList";
4
5
const Index = ({ title, description, posts, ...props }) => {
6
return (
7
<Layout title={title}>
8
<h1>{title}</h1>
9
<p>{description}</p>
10
<main>
11
<PostList posts={posts} />
12
</main>
13
</Layout>
14
);
15
};
16
17
export const getStaticProps = async () => {
18
const siteConfig = await import("../site.config.json");
19
20
const posts = ((context) => {
21
const keys = context.keys();
22
const values = keys.map(context);
23
24
const data = keys.map((key, index) => {
25
let slug = key.replace(/^.*[\\\/]/, "").slice(0, -3);
26
const value = values[index];
27
const document = matter(value.default);
28
return {
29
slug,
30
frontmatter: document.data,
31
markdownContent: document.content,
32
};
33
});
34
return data;
35
})(require.context("../posts", true, /\.md$/));
36
37
return {
38
props: {
39
title: siteConfig.title,
40
description: siteConfig.description,
41
posts,
42
},
43
};
44
};
45
46
export default Index;
47

Create just another markdown post to have more on the list. You should now have on your browser when navigate to index page

Index List Screen

And that's finally it! I will leave styling for another post. If you prefer you can also check or download source code from my github.
If you like this post, click Like button below. You can also share your comments or suggestions with me.


Like