Serve Markdown with a Next.js Server


Let's setup Next.js to be able to serve markdown content, such as blog posts.

Next.js Custom Server

Next.js has a nice feature set out of the box. If you have a site that's more on the site end of the spectrum (rather than the app end), you'll likely be able to use the next server as-is.

If, however, you'd like to do app-ish things on the backend, you'll likely need your own custom server. This is not hard.

Serving markdown seems to be one of the things that needs a custom server. I tried rendering the Markdown straight from React components, and it didn't seem to work. This after adding custom markdown loaders to the webpack config and all that. It still didn't work. The big hang up, for some unknown reason, was that the markdown modules could not be found in order to be imported correctly.

My solution was to build a custom server.

Basic Custom Server Setup

In Next.js terms, a custom server is simply some JavaScript that starts the next app in addition to doing any of the extra app stuff you need.

First, change your package.json start script:

{
  "scripts": {
    "start": "node server.js"
  }
}

Then do the basics in your server.js to initialize next:

const { createServer } = require('http')
const next = require('next')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

;(async _ => {
  await app.prepare()
  createServer(async (req, res) => {
    // ...more custom stuff here later

    return handle(req, res, parsedUrl)
  })
  .listen(process.env.PORT || 3000, (err) => {
    if (err) throw err
    console.log('> Ready on http://localhost:' + (process.env.PORT || 3000))
  })
})()

This can be done in Express, Koa, or any other http server library. This example uses the Node built-in http.createServer. Note that all this is doing is creating a next app, then using that app's request handler to handle all incoming requests. Custom additions are yet to come.

Reading Markdown

Since we're on the server, we have full access to the Node API. This includes the fs filesystem API. To get our markdown content, let's read markdown files out of a posts directory. Once the file strings are read, let's use a library called front-matter to parse metadata out of the front matter (not the markdown content yet). First install:

npm install front-matter

And let's put all this in a posts-repo.js:

const path = require('path')
const { promisify } = require('util')
const frontMatter = require('front-matter')
const fs = require('fs')

const readdir = promisify(fs.readdir)
const readFile = promisify(fs.readFile)

const deserialize = parsed => ({
  title: parsed.attributes.title,
  date: parsed.attributes.date,
  body: parsed.body
})

async function fetch() {
  const postsDir = path.join(__dirname, 'posts')
  const filenames = await readdir(postsDir)
  
  return Promise.resolve(filenames.map(async filename => {
    const markdown = await readFile(path.join(postsDir, filename), 'utf-8')
    const parsed = frontMatter(markdown)
    return deserialize(parsed)
  }))
}

exports.fetch = fetch

Serving Markdown

Now we have a module that can read and load into memory all the parsed markdown files. Now we have to serve this markdown with our custom server.

First, we need to detect that our client is making a request for the markdown. We'll do an overly-simple check of the request path. If the request url matches '/api/', we'll assume we're asking for this single endpoint to fetch all the markdown posts. We can make this more complicated later.

In server.js, we'll make the adjustments. Inside the createServer callback, we don't just pass everything on to the Next.js handle function any more. We now parse the URL and call our postsRepo.fetch instead:

const { parse } = require('url')

const postsRepo = require('./posts-repo')

const serialize = data => JSON.stringify({ data })

// ...

  createServer(async (req, res) => {
    const parsedUrl = parse(req.url, true)
    const { pathname } = parsedUrl

    if (pathname.includes('/api/')) {
      const posts = await postsRepo.fetch()

      res.writeHead(200, { 'Content-Type': 'application/json'})
      return res.end(serialize(posts))
    }

    return handle(req, res, parsedUrl)
  })

Requesting Markdown from Next.js Component

Now we have an endpoint that can serve the markdown-based posts. We need to get the React components to request the data they need. In Next.js, this is done on the server and in the client. We'll choose an http library that can function in both places. We'll also install a markdown parser:

npm install isomorphic-fetch react-markdown

We're ready to rock and roll in our React component. The top-level component in Next.js, called a "page", has a special hook called getInitialProps. Here in pages/indexx.js we can do our data fetching:

import Post from '../components/post'

export static class Index extends React.Component {
  static async getInitialProps({ query }) {
    const posts = await Post.fetch()
    return { posts }
  }
  render() {
     {this.props.posts.map(p => )}
  }
}

And then components/post.js:

import 'isomorphic-fetch'
import Markdown from 'react-markdown'
import React from 'react'

const Post = props =>
  <div>
    <h1 className="title">{props.title}</h1>
    <time className="date" dateTime={props.date}>{props.date}</time>
    <Markdown source={props.body} />
  </div>

class PostContainer extends React.Component {
  static async fetch() {
    const res = await fetch('<yourHost?/api/v1/posts')
    const json = await res.json()
    return json ? json.data : {}
  }
  render() {
    return <Post {...this.props.post} />
  }
}

export default PostContainer

Here we have the http code to fetch the post from the server, and we're finally rendering markdown.

There are a few steps here, aren't there. We can still get the coolness of Next.js' server-rendering of React while doing our own custom markdown fetching on top of it.

Is this how you render markdown content in Next.js? How have you improved upon the method?