DEV Community

James Wallis
James Wallis

Posted on • Edited on

How to use markdown rather than code to create pages in a Next.js project

Originally titled: Using Markdown and the Remark converter to make a dynamic and maintainable Next.js project

This blog part one of a two part extension to my rebuild of the Wallis Consultancy website. It documents how to use Markdown files with Next.js to dynamically create pages at build time and use Netlify CMS to make them editable without having to download the source code to your local machine.

The finished website (hosted on GitHub Pages): https://wallisconsultancy.co.uk
The source code: https://github.com/james-wallis/wallisconsultancy

 Background

When I rebuilt wallisconsultancy.co.uk using Next.js a couple of months ago, one of the reasons for doing so was to make it more maintainable than it was when it was implemented via PHP and ran in a Docker container. While on a whole I achieved this goal, making changes to the content is still overcomplicated and means modifying a React.js file in the pages directory which Next.js uses to separate pages.

Writing a page using Markdown leads to a much better experience than using React.js. For starters, you write in plain text and don't need to worry about opening tags and closing tags or classNames for styling, you can just focus on the content.

Therefore in this blog I will demonstrate how to configure a Next.js website to use Markdown for the content of each page and dynamic routes so that you can use a generic layout for each page. By combining these, we will end up with a much more maintainable website that has only two files in the pages directory and has content that is changed solely by editing a Markdown file on a per page basis.

 Plan

To use the following to create a website which gets it's content from Markdown files:

We'll creating two pages (in the pages directory):

  1. index.js - for the home page
  2. [slug].js - for every other page

AFAIK it's not possible to have / as a dynamic route through Next.js - if you know otherwise reach out and I'll append this post!

Note: It is possible to do "catch all routes" which should allow you to do subpaths in URLs but for this tutorial I'll only explain how to do top level paths ('/', '/about'). To read more about catch all routes checkout the Next.js docs.

Let's do it

We need to install some packages so that we can convert the Markdown to HTML. Run npm i -s unified remark-parse remark-react gray-matter.

Next we need to create some helper functions that our Next.js application can call to get a list of all the Markdown files in our directory and the contents files. Create a utils file for these functions and paste in the following JavaScript. I normally put these in a lib directory and have called the file markdown.js (view on GitHub).

import fs from 'fs';
import { join } from 'path';
import matter from 'gray-matter';

/**
 * _pages and _pages/dynamic directory where the markdown content will live
 * _pages will have the home.md (aka index or /)
 * _pages/dynamic will be home to all other pages (aka [slug].js)
 */
const pagesDirectory = join(process.cwd(), '_pages');
const dynamicPagesDirectory = join(pagesDirectory, 'dynamic');

/**
 * Gets all the files (slugs) in a directory
 */
export function getSlugsFromDirectory(dir) {
  return fs.readdirSync(dir);
}

/**
 * Gets the contents of a file
 * The gray-matter (metadata at the top of the file) will be
 * added to the item object, the content will be in
 * item.content and the file name (slug) will be in item.slug.
 */
export function getBySlug(dir, slug, fields = []) {
  const realSlug = slug.replace(/\.md$/, '');
  const fullPath = join(dir, `${realSlug}.md`);
  const fileContents = fs.readFileSync(fullPath, 'utf8');
  const { data, content } = matter(fileContents);

  const items = {};

  // Ensure only the minimal needed data is exposed
  fields.forEach((field) => {
    if (field === 'slug') {
      items[field] = realSlug;
    }
    if (field === 'content') {
      items[field] = content;
    }

    if (data[field]) {
      items[field] = data[field];
    }
  });

  return items;
}

/**
 * Returns contents of a page in the _pages directory
 */
export function getPageContentBySlug(slug, fields = []) {
  return getBySlug(pagesDirectory, slug, fields);
}

/**
 * Returns contents of a page in the _pages/dynamic directory
 */
export function getDynamicPageContentBySlug(slug, fields = []) {
  return getBySlug(dynamicPagesDirectory, slug, fields);
}

/**
 * Returns a list of all the pages in the _pages/dynamic directory
 */
export function getAllDynamicPages(fields = []) {
  const slugs = getSlugsFromDirectory(dynamicPagesDirectory);
  const pages = slugs.map((slug) => getDynamicPageContentBySlug(slug, fields));
  return pages;
}
Enter fullscreen mode Exit fullscreen mode

Copy that JavaScript into your project and then we'll be ready to create our dynamic page!

Creating the dynamic page ([slug].js)

Using Next.js dynamic pages, we will add functionality to create a separate page for each Markdown file in the _pages/dynamic directory while only needing to create a single file in the pages directory.

To do this we will need to use a couple of Next.js functions:

  1. getStaticPaths: This function is used to tell Next.js what URL paths are going to be rendered, so in this function we will call getAllDynamicPages from the markdown.js file above.
  2. getStaticProps: This function is used to get additional props for a page at build time, so in this function will receive the slug (file path) to render and we will pass it into the getDynamicPageContentBySlug to get the metadata and contents for a page.

Create a page called [slug].js in your pages directory with the following contents:

import PrintMarkdown from '../components/markdown/printMarkdown';
import { getDynamicPageContentBySlug, getAllDynamicPages } from '../lib/markdown';

export default function DynamicPage({ page }) {
  const {
    title,
    description,
    slug,
    content,
  } = page;

  return (
    <div>
      <h1>{title}</h1>
      <h2>{description}</h2>
      {/* we'll go into the PrintMarkdown component later */}
      <PrintMarkdown markdown={content} />
    </div>
  );
}

export async function getStaticProps({ params }) {
  const { slug } = params;

  // Pass in the fields that we want to get
  const page = getDynamicPageContentBySlug(slug, [
    'title',
    'description',
    'slug',
    'content',
  ]);

  return {
    props: {
      page: {
        ...page,
      },
    },
  };
}

export async function getStaticPaths() {
  const posts = getAllDynamicPages(['slug']);
  const paths = posts.map(({ slug }) => ({
    params: {
      slug,
    },
  }));
  return {
    paths,
    fallback: false,
  };
}
Enter fullscreen mode Exit fullscreen mode

and create a Markdown (.md) file in the _pages/dynamic directory at the top level of your project containing the following Markdown:

---
title: "Hello dynamic world!"
description: "My first dynamic Page"
---

# Heading 1

A paragraph with some **bold text**.
Enter fullscreen mode Exit fullscreen mode

If you run that, Next.js will throw an error because the PrintMarkdown component doesn't exist.

 PrintMarkdown Component

Let's create a component which can turn out Markdown into React!

Create a file in your components directory and call it PrintMarkdown with the following contents:

import unified from 'unified';
import parse from 'remark-parse';
import remark2react from 'remark-react';
import markdownStyles from './markdown-styles.module.css';

export default function PrintMarkdown({ markdown }) {

  // Convert the Markdown into React
  const content = unified()
    .use(parse)
    .use(remark2react)
    .processSync(markdown).result;

  return (
    <div className={markdownStyles.markdown}>
      {content}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This file will turn the Markdown into React and add it to the DOM. It uses css-modules to style the React. I won't go into what styles I use but you can find the Wallis Consultancy styles on GitHub.

Once you've added all of the above, you should be able to run your Next.js project and see a page displaying your title, description and markdown.

Note: Any links that you use to navigate to other parts of your website in the Markdown will be converted to <a> tags rather than Next.js <Link> tags. I've written a short post demonstrating how to do this.

We're almost done creating out dynamic website - you should be able to create more Markdown files in the _pages/dynamic directory and then reach them in your browser when Next.js is running. To finish, we just need to create the index page (/) separately to the dynamic pages as you will find that if you create a file with the name index.md it will not work for the home page (the URL will be /index, not great).

The Index page (/)

The index page will be like the dynamic page above but instead of using the getStaticPaths Next.js function we will hardcode the slug for getPageContentBySlug function call in getStaticProps to home so that it reads the Markdown file _pages/home.md (not _pages/dynamic as we're calling getPageContentBySlug).

First create the Markdown file in the _pages directory and give it the contents:

---
title: Home
description: "Your home page"
---

# Home page

This is your home page
Enter fullscreen mode Exit fullscreen mode

Next, create a new file in your pages directory called index.js and give it the following content:

import PrintMarkdown from '../components/markdown/printMarkdown';
import { getDynamicPageContentBySlug, getAllDynamicPages } from '../lib/markdown';

export default function IndexPage({ page }) {
  const {
    title,
    description,
    slug,
    content,
  } = page;

  return (
    <div>
      <h1>{title}</h1>
      <h2>{description}</h2>
      <PrintMarkdown markdown={content} />
    </div>
  );
}

export async function getStaticProps() {
  // Here we're using the getPageContentBySlug 
  // as opposed to getDynamicPageContentBySlug
  // We're also passing in the string 'home' to tell it 
  // we want to use the _pages/home.md file for the 
  // page props
  const page = getPageContentBySlug('home', [
    'title',
    'description',
    'slug',
    'content',
  ]);

  return {
    props: {
      page: {
        ...page,
      },
    },
  };
}

Enter fullscreen mode Exit fullscreen mode

Once you've created and populated those two files, your home page should be available!

Round Up

In this blog we've configured our Next.js application to use build it's routes using Markdown files and we've made it dynamic so that we only need to maintain one file! Okay, two files ([slug].js, and index.js) but the root path is an exception (if you can make this dynamic too, leave a comment and I'll update the tutorial!).

In second part of this two part series, I'll add Netlify CMS to the Wallis Consultancy website so that it can be used to create and modify pages dynamically on the website.

Remember: The links that the remark-react project creates will be an <a> tag and not the Next.js <Link> tag. To make them use <Link> tags for local links and <a> for external links you'll need to use a custom component - I've written a short post demonstrating how to do this.

Top comments (1)

Collapse
 
bluefalconhd profile image
Hayes Dombroski

Hey! I am getting an error when running some of this code:

Error: Missing `createElement` in `options`
    at Function.remarkReact (file:///Users/hayesdombroski/programs/centauri/website/node_modules/remark-react/lib/index.js:48:11)
    at Function.freeze (file:///Users/hayesdombroski/programs/centauri/website/node_modules/unified/lib/index.js:136:36)
    at Function.processSync (file:///Users/hayesdombroski/programs/centauri/website/node_modules/unified/lib/index.js:435:15)
    at PrintMarkdown (webpack-internal:///./components/printMarkdown.js:28:184)
    at renderWithHooks (/Users/hayesdombroski/programs/centauri/website/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5658:16)
    at renderIndeterminateComponent (/Users/hayesdombroski/programs/centauri/website/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5731:15)
    at renderElement (/Users/hayesdombroski/programs/centauri/website/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5946:7)
    at renderNodeDestructiveImpl (/Users/hayesdombroski/programs/centauri/website/node_modules/react-dom/cjs/react-dom-server.browser.development.js:6104:11)
    at renderNodeDestructive (/Users/hayesdombroski/programs/centauri/website/node_modules/react-dom/cjs/react-dom-server.browser.development.js:6076:14)
    at renderNode (/Users/hayesdombroski/programs/centauri/website/node_modules/react-dom/cjs/react-dom-server.browser.development.js:6259:12)
error - Error: Missing `createElement` in `options`
    at Function.remarkReact (file:///Users/hayesdombroski/programs/centauri/website/node_modules/remark-react/lib/index.js:48:11)
    at Function.freeze (file:///Users/hayesdombroski/programs/centauri/website/node_modules/unified/lib/index.js:136:36)
    at Function.processSync (file:///Users/hayesdombroski/programs/centauri/website/node_modules/unified/lib/index.js:435:15)
    at PrintMarkdown (webpack-internal:///./components/printMarkdown.js:28:184)
    at renderWithHooks (/Users/hayesdombroski/programs/centauri/website/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5658:16)
    at renderIndeterminateComponent (/Users/hayesdombroski/programs/centauri/website/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5731:15)
    at renderElement (/Users/hayesdombroski/programs/centauri/website/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5946:7)
    at renderNodeDestructiveImpl (/Users/hayesdombroski/programs/centauri/website/node_modules/react-dom/cjs/react-dom-server.browser.development.js:6104:11)
    at renderNodeDestructive (/Users/hayesdombroski/programs/centauri/website/node_modules/react-dom/cjs/react-dom-server.browser.development.js:6076:14)
    at renderNode (/Users/hayesdombroski/programs/centauri/website/node_modules/react-dom/cjs/react-dom-server.browser.development.js:6259:12) {
  page: '/posts/[slug]'
}
Enter fullscreen mode Exit fullscreen mode