DEV Community


Creating a Markdown Blog with Next.js

kendallstrautman profile image Kendall Strautman Updated on ・10 min read

Originally published on the TinaCMS blog

04.01.20: This blog has been updated to reflect the latest data fetching methods provided by Next.js 9.3. The first iteration of this blog used getInitialProps. The examples and repo now use getStaticProps.

Next.js is a React "metaframework" (a framework built on a framework) for developing web applications. Next.js has become a popular choice for web developers due to its bootstrapped React environment (similar to create-react-app) and its simple, file-based routing for writing backend code.

Next.js is simple and flexible. Compared to a full-fledged static site generator, there are less prescriptive guiderails placed on developers in the implementation of an app or site. Due to this flexibility, this article shares just one perspective to approach building a simple, markdown-based blog. Take what’s helpful 🤗, disregard the rest.

If you'd like to skip ahead and reference final versions of the starter, feel free to checkout the finished implementation.

Clone the starter

Let’s get started. I have provided a bare bones starter to use as a starting point for this tutorial. You can clone the project or check it out on github for reference.

// clone the repo from your terminal
$ git clone my-nextjs-blog

// install the dependencies
$ cd my-nextjs-blog
$ yarn install

// start up the dev server
$ yarn dev
Enter fullscreen mode Exit fullscreen mode

After you clone the project and start the dev server, navigate to http://localhost:3000/ in your browser to see what we're working with.


As you can see, it's pretty simple at the moment. If you look at the project in your code editor, you will see the following directory structure:

Enter fullscreen mode Exit fullscreen mode

Project Structure

Let’s look at the pages/index.js file:

const Index = props => {
  return (
        <BlogList />

export default Index

export async function getStaticProps() {
  const configData = await import(`../data/config.json`)
  return {
    props: {
      title: configData.title,
      description: configData.description,
Enter fullscreen mode Exit fullscreen mode

You’ll see that we have a Layout component wrapping a <section> with a BlogList component — these are all the pieces that render our little starter so far.

Data Handling

Next.js pre-renders every page, meaning it generates HTML for pages in advance. As of Next.js 9.3, there are two ways to pre-render pages: static generation or server-side-rendering (SSR). Next.js is unique in that you can use either approach depending on the project.

For this blog, we will implement static generation, this means HTML pages for each route will be generated at build time. Static generation allows pages to be cached by a CDN, improving performance.


In the initial exampleindex.js, notice the use of getStaticProps below the component. This function allows us to fetch data and return it as props to our page component. The page will be rendered at build time with the props from the return object in getStaticProps.

This is your bread and butter for retrieving page-level data in Next. You can use getStaticProps to fetch data from an external api, or as seen in this example, you can get a hold of local data sources.

Note: this method only works for components defined in the pages/ directory, i.e., page components. You cannot use this method on child components, but you can pass down the data received to these child components, as you see being done with Layout in the example above.

Layout is being passed props such as the site title and description. If you look at the data in data/config.json, you’ll see the values these props are referencing. Go ahead and change the site title to your project name, then watch it update in the header.

Layout & Styling 🦋

To zoom out a little, the purpose of the Layout component is to provide the visual skeleton for every page of the site. It typically will contain some sort of nav or header that shows up on most or all pages, along with a footer element. In our case we just have a header that contains the site title.

Within Layout, there is a Meta component that contains all global styles along with anything needed to be put in the head of the site for SEO or accessibility purposes. Note that the use of a Layout component isn’t unique to Next.js; you’ll see it commonly used in Gatsby sites as well.

One thing you may notice in the Layout component is the use of a <style jsx> tag. Next.js works out of the box with styled-jsx, a neat css-in-js framework made by the ZEIT team. It’s super intuitive to use. All of the styles are scoped to the component, and you can do dynamic styling based on props. The css-in-js world is your oyster!

The only downside of styled-jsx is the lack of support for nesting, which may or may not be a concern for you. As long as you just write good ol’ fashioned vanilla css, you’ll be good. To learn more about how to use styled-jsx, take a look at the styled-jsx GitHub repository.

Note again that global styles and fonts are handled in the Meta component via the <style jsx global> tag. Use this anywhere you need to implement global styles.

Adding the Posts Directory

Now that we’re familiar with the structure of the project and Next.js fundamentals, let’s start adding the pieces and parts to get the Markdown blog up and running.

First, add a new folder in the root of your project called posts. You can add all your Markdown blog posts here. If you don’t already have content ready, just add a few dummy blog posts. I like to use Unsplash for sample photos and Cupcake, Hipsum, or Sagan Ipsum are my preferred text generators — keeps things interesting 🧁.

Here’s an example filler blog post with some commonly used frontmatter values.

title: "The coastal red giants"
author: Watson & Crick
date: 2019-07-10
hero_image: ../static/bali-15.jpg
Brain is the seed of intelligence something incredible is waiting to be known.
Enter fullscreen mode Exit fullscreen mode

Also, create a publicfolder in the root. This is where you will keep images.

Note: The static folder has been deprecated in favor of public. To make the switch without changing file paths, create a public directory and move static into it.

Processing Markdown Files 🤖

Next, we need to install a few packages that will process our Markdown files.

$ yarn add raw-loader gray-matter react-markdown
Enter fullscreen mode Exit fullscreen mode

Raw Loader will process our Markdown files. Gray Matter will parse our yaml frontmatter values. And React Markdown will parse and render the body of our Markdown files.

Add Next.js Config

Now that we’ve installed some packages needed to handle Markdown, we need to configure the use of the raw-loader by creating a next.config.js file at the root of the project. In this file we will handle any custom configuration for webpack, routing, build & runtime config, export options, and a whole lot more. In our use case, we will simply be adding a webpack rule to use raw-loader for processing all Markdown files.

module.exports = {
  webpack: function(config) {
      test: /\.md$/,
      use: 'raw-loader',
    return config
Enter fullscreen mode Exit fullscreen mode

Pages & Dynamic Routing

So we’re set up to use Markdown files in our project. Let’s start coding a blog template page that will render the content from these Markdown files in posts.

For some background knowledge, the pages directory is special in Next.js. Each .js file in this directory will respond to a matching HTTP request. For example, when the home page ('/') is requested, the component exported from pages/index.js will be rendered. If you wanted your site to have a page at /about, simply create a file named pages/about.js.

This is awesome for static pages, but we'd like to have a single template from which all blog posts will be built, sourcing the different data from each Markdown file. This means we need some sort of dynamic routing, such that unique blog posts utilizing the same template have ‘pretty’ urls and their own individual pages.

Dynamic routes in Next.js are identified by square brackets [] in the filename. Within these brackets we can pass a query parameter to the page component. For example, let’s create a new folder within pages called blog, then add a new file within that blog folder [slug].js, we can use whatever is passed as this slug parameter to dynamically access data. So if we visit http://localhost:3000/blog/julius-caesar, whatever is returned from the [slug].js page component will render, and will have access to that ‘slug’ query parameter, i.e. ‘julius-caesar’.

Get Markdown Data For the Blog Template

With dynamic routing, we can make use of this slug parameter by passing in the filename of the blog post and then getting the data from the corresponding Markdown file via getStaticProps.

import matter from 'gray-matter'
import ReactMarkdown from 'react-markdown'
import Layout from '../../components/Layout'

export default function BlogTemplate(props) {
  // Render data from `getStaticProps`
  return (
    <Layout siteTitle={props.siteTitle}>
          <ReactMarkdown source={props.markdownBody} />

export async function getStaticProps({ ...ctx }) {
  const { slug } = ctx.params
  const content = await import(`../../posts/${slug}.md`)
  const config = await import(`../../data/config.json`)
  const data = matter(content.default)

  return {
    props: {
      siteTitle: config.title,
      markdownBody: data.content,

export async function getStaticPaths() {
  //get all .md files in the posts dir
  const blogs = glob.sync('posts/**/*.md')

  //remove path and extension to leave filename only
  const blogSlugs = =>
      .replace(/ /g, '-')
      .slice(0, -3)

  // create paths with `slug` param
  const paths = => `/blog/${slug}`)

  return {
    fallback: false,
Enter fullscreen mode Exit fullscreen mode

Notice in this example that we’re making use of gray-matter and ReactMarkdown to properly handle the YAML frontmatter and Markdown body.

A zoomed out look at how this is working: when you navigate to a dynamic route, .e.g. http://localhost:3000/blog/julius-caesar, the BlogTemplate component in pages/blog/[slug].js is passed the params object { slug: ‘julius-caesar’ }. When the getStaticProps function is called, that params object is passed in through the context. We get ahold of that slug value and then go search for a file within the posts directory that contains the same filename. Once we get the data from that file, we parse the frontmatter from the Markdown body and return the data. That data is passed down as props to the BlogTemplate component which can then render that data as it needs.


At this point, you may be more familiar with getStaticProps, but this function should look new — getStaticPaths. Since this template uses dynamic routes, we need to define a list of paths for each blog, so all the pages will be rendered statically at build time.

In the return object from getStaticPaths, two keys are required: paths and fallback. paths should return an array of pathnames and any params used in the page name. For example the 'param' used in /blog/[slug].js is 'slug'. You should only need to use getStaticPaths for dynamic routing.

The fallback property allows you to control the behavior if a path is not returned from getStaticPaths. We set this to false so that unreturned paths will show a 404 page.

Before the release of Next.js 9.3, this path generation for static export could be handled via exportPathMap.

Checkout the [slug].js file in the final version of my starter blog to get another idea of how that blog data could be rendered and styles applied.

Get Data For the Blog Index

Let’s finish this simple blog off by adding in the proper data to the BlogList component for the Index page. Since we can only use getStaticProps on page components, we will get a hold of all the blog data in the Index component and then pass it down as a prop for BlogList to render.

// pages/index.js
export async function getStaticProps() {
  const siteConfig = await import(`../data/config.json`)
  //get posts & context from folder
  const posts = (context => {
    const keys = context.keys()
    const values =

    const data =, index) => {
      // Create slug from filename
      const slug = key
        .replace(/^.*[\\\/]/, '')
        .slice(0, -1)
      const value = values[index]
      // Parse yaml metadata & markdownbody in document
      const document = matter(value.default)
      return {
        markdownBody: document.content,
    return data
  })(require.context('../posts', true, /\.md$/))

  return {
    props: {
      allBlogs: posts,
      title: siteConfig.default.title,
      description: siteConfig.default.description,
Enter fullscreen mode Exit fullscreen mode

This can be slightly complex to look at, but let’s take it one step at a time. Feel free to reference this blog for the original code. It uses a function provided by Webpack, require.context(), that allows us to create our own ‘context’ based on three parameters:

  • The directory to match within.
  • A boolean flag to include or exclude subdirectories.
  • A regular expression to match files against.
require.context(directory, (useSubdirectories = false), (regExp = /^\.\//))
Enter fullscreen mode Exit fullscreen mode

Creating a ‘context’ allows us to create a space essentially where we can pick out all the files matching a regular expression from a particular directory, and manipulate them into manageable formats that are provided back to the component as props to be rendered.

Now that we have all of the blog data, pass it as a prop to the BlogList component.

const Index = props => {
  return (
        <BlogList allBlogs={props.allBlogs} />

export default Index
Enter fullscreen mode Exit fullscreen mode

Then you are free to loop through the blogs and render the list within your BlogList component as you need. Feel free to check out the BlogList component in my starter to see how that data could be handled.

Next Steps

Checkout the final repo! 🥳

After setting up your blog or portfolio site, you’ll most likely need a content management system to make editing and updating your posts or data easier. Stay tuned for my next blog on setting up this starter with TinaCMS. In the meantime, you can check out our documentation on using Next.js with TinaCMS, or fork the finished Next+Tina blog to start playing with TinaCMS right away.

Discussion (13)

Editor guide
wholesomesj profile image
SeungJin YOUN

I'm appreciating your app.
I could try a migration to your blog app.
However, after all migrations, sudden errors come out...
The matter is, I could not figure it out or fix it...

the errors come out after my all previous markdown posts migrations.
however, the funny thing is, last night when I complete all mitration and deploy on
'now', that was no problem at all.. but after a sleep,
you can check the deployment was successful last night.

Do you have any idea of this?
Thank you!

$ npm start

> next-blog@3.2.1 start C:\sj-log
> node server.js

Defining routes from exportPathMap
(node:18352) UnhandledPromiseRejectionWarning: TypeError: Missing pattern at 27
    at lexer (C:\sj-log\node_modules\path-to-regexp\dist\index.js:82:23)
    at parse (C:\sj-log\node_modules\path-to-regexp\dist\index.js:97:18)
    at stringToRegexp (C:\sj-log\node_modules\path-to-regexp\dist\index.js:329:27)
    at pathToRegexp (C:\sj-log\node_modules\path-to-regexp\dist\index.js:403:12)
    at Object.match (C:\sj-log\node_modules\path-to-regexp\dist\index.js:248:14)
    at C:\sj-log\node_modules\next\dist\next-server\server\lib\path-match.js:6:42
    at DevServer.addExportPathMapRoutes (C:\sj-log\node_modules\next\dist\server\next-dev-server.js:5:41)
(node:18352) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 2)
(node:18352) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
kendallstrautman profile image
Kendall Strautman Author

Hi there! Not sure about this error. The finished sample repo is deployed fine for me on Netlify right now. Maybe make sure the build command is correct? It's hard to tell without more information!

jplew profile image
JP Lew • Edited

Hey Kendall, I'm building my blog right now and found this useful, especially your finished product repo. Didn't know you needed an exportPathMap in the next.config.js so thanks for unblocking me.

I ended up going with frontmatter-markdown-loader for md parsing and Netlify CMS for versioning:

Tina looks pretty cool might check it out.

kendallstrautman profile image
Kendall Strautman Author

Nice! Glad it was helpful. Yes that's a good point, I should follow up with notes on using exportPathMap ☺️

stschindler profile image
Stefan Schindler • Edited

How do you handle post ordering? Easiest I can think of is just prefixing every post filename with a date, but I don't really like adding meta data to the filename.

Another simple idea is adding the publication date as front matter and then sorting all posts when reading them. Might be a little slow though.

Any ideas?

Nice article!

kendallstrautman profile image
Kendall Strautman Author • Edited

Yeah I think both methods would work! Agree that the first would be simplest. I've implemented the latter on the site for the blog index if you wanna check it out. You can also see the pagination implementation there so you only render a certain number of posts per page.

didaquis profile image
Dídac García

Well explained!

I followed your tutorial in addition to this one:

I noticed that my code, the Netlify tutorial code and also yours have the same problem. If you open the development console of your browser and visit your site, you will see some 404 errors. The site works perfectly, but the errors are there! :-(

You can read more about issue that here and here 3294 but I can't solve it.

Do you have any idea about how to solve it?

kendallstrautman profile image
Kendall Strautman Author • Edited

You probably need to use getStaticPaths where you are implementing dynamic routes. (exportPathMap is now discouraged). Or there may be something funky going on with the parameters you are passing the routes. I would also look into the build command you are using for production. Just recently I changed from using next export to just next build now that you can statically export at the page level with getStaticProps.

oriuken profile image

Hey, Great Article ! Thanks so much...
One question though... if you are generating all Blog Post as static content at Building time, does it means that new Posts won't be shown until you Build again ?

Thanks !

kendallstrautman profile image
Kendall Strautman Author

Yeah that's correct! As far as I know, this is typical with JAMstack sites, when the content is updated the site will be rebuilt.

yougotwill profile image
William Grant

Great article! Thanks for writing.

denmasgie profile image
Mei Giyanto

Thank you so much