Static website using NextJS and Markdown

Gatsby and NextJS are popular alternatives to create static websites using React. This tutorial shows how to work with NextJS. We want to take some pages written in markdown and convert them to static pages. Along the way, we will explore NextJS features.

NextJS App Overview

The sample app is available in Netlify. It is a company website that offers development services. There is a Home page that has a background image and a link to Services page. Services page lists all the services that the company provides. The user can navigate to the respective service page by clicking on the link. Each service offering is available as a markdown file that we convert to a static page when we build the app. The NextJS features that we explore are:

  • Create multiple pages including a dynamic page.
  • Style the components using CSS modules.
  • Store the images in a public folder.
  • Build a Layout page that has a nav bar.
  • Add the title element to all pages.
  • Use getStaticProps() and getStaticPaths() that works at build time.
  • Convert Markdown content to HTML.
  • Build a static site

At the time of writing this article in 2019, NextJS was at version 8. I updated the page in 2021 to upgrade everything to NextJS version 11.

A) Create New Project

Create a new project by using create-next-app.

npx create-next-app nextdemo

Run the start script using “npm start”. Open localhost:3000 in a browser.

B) Update home page

In the pages folder, update the index.js file with the following code:

import Link from 'next/link';
import styles from '../styles/home.module.css';

export default function Index() {
    return (
        <div className={styles.home}>
            <div className={styles.center}>
                <div className={styles.company}>Custom React Development</div>
                <Link href="/services">
                    <button className={styles.button}>View our services</button>
                </Link>
            </div>
        </div>
    );
}

In the home page, we have some big text and a button. The button is within a Link component. Link is a NextJS component that navigates to a new page when the user clicks on it.

Notice the import from ‘home.module.css’. By default, we use CSS modules to style a page or a component.

C) Style the page with CSS module

Create a new styles folder and within that, create a new file: home.module.css. Whenever the file ends with “.module.css”, NextJS interprets that as a CSS module. Within that file, we define normal styles. After that, we import the CSS module in our component file. CSS modules allows the developer to reference the styles with a special syntax like “className={styles.home}”.

In home.module.css, use the following code:

.home {
    width: 100%;
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    background: url('/images/bg.jpeg') no-repeat center center fixed;
    background-size: cover;

}

.center {
    text-align: center;
    color: white;
}

.company {
    font-size: 3rem;
    margin-bottom: 1rem;
}

.button {
    background-color: #3e00ff;
    padding: 10px 20px;
    font-size: 20px;
    border: none;
    outline: none;
    color: white;
    cursor: pointer;
    border-radius: 4px;
    transition: all 200ms;
}

.button:hover {
    background-color: #ae00fb;
}

The styles within CSS module are standard styles that we write in any other app. Note that, by convention, there is a one-to-one relation between a component and its CSS module. Usually, we don’t use the same CSS module for another component. Also, styles folder is a place where I like to keep my page related styles as well as global styles.

Notice that the home class has a background image. We will see how we can store images in our NextJS app.

D) Public folder

Previously, we used to store images in a special static folder. But now, we don’t need a static folder. NextJS has made its features similar to “create-react-app” (CRA). So, we have a public folder where we place all images.

Within the public folder, create another folder images. Place this file into that folder.

With these changes, there should be no build errors. It is time to modify the special _app.js file in the pages folder.

E) Create a Layout component

Update _app.js file in pages folder with the following code:

import Layout from '../components/layout';
import '../styles/globals.css';

function MyApp({ Component, pageProps }) {
    return (
        <Layout>
            <Component {...pageProps} />
        </Layout>
    );
}

export default MyApp;

_app.js is a special file that allows us to add a Layout component that wraps all the page components. It also allows us to import global styles. We define the global styles in a normal CSS file (no CSS modules here). So, the styles in the CSS file applies to the entire app.

Within the styles folder, create a globals.css file with the following code.

body {
    margin: 0;
    padding: 0;
    font-family: Arial, Helvetica, sans-serif;
    background-color: #170055;
}

.container {
    padding: 5rem 1.5rem 2rem 1.5rem;
    color: #fff;
    max-width: 40rem;
}

h1 {
    font-size: 2rem;
    color: #b5ffd9;
}

h2 {
    font-size: 1.2rem;
}

p {
    color: #ccc;
    font-size: 1rem;
}

Now, it is time to define our Layout component. But where do we place it? We can create a new folder with name “components” that is at the same level as the pages folder. In the components folder, create a new file, “Layout.js” with the following code:

import styles from './layout.module.css';
import Link from 'next/link';
import { Fragment } from 'react';

export default function Layout(props) {
    return (
        <Fragment>
            <header className={styles.header}>
                <div className={styles.wrapper}>
                    <h1 className={styles.title}>
                        <Link href="/">
                            <a className={styles.titleLink}>
                                Vijay Consulting Services
                            </a>
                        </Link>
                    </h1>
                    <div>
                        <ul className={styles.menu}>
                            <li>
                                <Link href="/">
                                    <a>Home</a>
                                </Link>
                            </li>
                            <li>
                                <Link href="/services">
                                    <a>Services</a>
                                </Link>
                            </li>
                        </ul>
                    </div>
                </div>
            </header>
            <main>{props.children}</main>
        </Fragment>
    );
}

Layout component has the company name and the menu. Menu contains two items, home page and Services page. We will define services page shortly. And then finally, the layout component contains the page component within a main tag.

Let’s now define the styles for the Layout component in a CSS module. Within the components folder, create a new file “layout.module.css” with the following code:

.header {
    position: fixed;
    width: 100%;
    background: rgba(62, 0, 255, 0.3);
    z-index: 1;
}

.wrapper {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem 1.5rem;
}

.title {
    margin: 0;
}

.titleLink {
    color: white;
    text-decoration: none;
}

.menu {
    list-style: none;
    padding: 0;
    margin: 0;
}

.menu li {
    display: inline;
    margin-right: 16px;
}

.menu li a {
    color: #e0e0e0;
    text-decoration: none;
}

.menu li a:hover {
    color: white;
    text-decoration: underline;
}

With these changes, our home page is complete and should like so:

Home page
Home page

F) Processing Markdown files

Let’s create a Services page that will list down all the service offerings that the company provides. Each service offering is available as a markdown file. Create a new folder, services at the same level as pages folder. Place the following markdown files in that folder.

We want to loop through the files in that folder and get two things: the title within the markdown file and the filename without the extension. Attach this information as props to the page level component that we create. Here is how we do it.

When we browse the Home page, the index.js file within the pages folder serves as the page component. What we want to do is browse to another page: localhost:3000/services. To create the page component for the services route, there are two approaches that we can take. Create a services.js file within the pages folder. Or create a new folder “services” within the pages folder and within that create an index.js file. We will adopt the second approach.

Update the pages/services/index.js file with the following code:

import Link from 'next/link';
import styles from '../../styles/services.module.css';

export default function Services({ services }) {
    return (
        <div className="container">
            <h1>Services</h1>
            <p>View our service offerings:</p>
            <ul className={styles.services}>
                {services.map((service) => (
                    <li key={service.slug}>
                        <Link href={`/services/${service.slug}`}>
                            {service.title}
                        </Link>
                    </li>
                ))}
            </ul>
        </div>
    );
}

The above code loops through a services prop. For each item, it creates a list item with a Link component. The link component has a reference to a service offering page that we are yet to create. But where do we get the services prop from?

Implement getStaticProps

We have to write some more code for that. NextJS provides a special function “getStaticProps()” that runs at build time. The build process runs this function. So, we have access to Node objects within the function. So, import the following modules at the top of the services file.

import fs from 'fs';
import { Converter } from 'showdown';

fs is a Node module. So, we don’t need to install it. But showdown is a package for processing markdown files. So, we got to install it.

npm install showdown // or
yarn add showdown

Export the getStaticProps function from the pages/services/index.js file:

export function getStaticProps() {
    const files = fs.readdirSync('services');
    const services = files.map((file) => {
        const service = file.slice(0, file.indexOf('.md'));
        const content = fs.readFileSync(`services/${service}.md`, 'utf8');
        const converter = new Converter({ metadata: true });
        converter.makeHtml(content);
        const meta = converter.getMetadata();
        const { title } = meta;
        return {
            slug: service,
            title,
        };
    });
    return {
        props: {
            services,
        },
    };
}

We use the fs module to read the contents of the services folder that has all the markdown file. Note that the build process runs this function. So, the present working directory is at the project root level. So, ‘services’ folder refers to the services folder that is at the same level as the pages folder.

We loop through each file in the services folder. And extract the file name without the extension. Next, we create the Converter object from the showdown package. The converter object creates HTML from the markdown file. It also extracts the title of the markdown file. We store the filename in the slug key. And the title in the title key of the props object.

So, the services prop gets its value during build time.

Finally, to complete the services page, create a new CSS module in the styles folder, ‘services.module.css’.

.services li {
    padding: 4px;
}

.services a:link,
.services a:visited {
    color: #ccc;
    text-decoration: none;
}

.services a:hover {
    text-decoration: underline;
    color: #fff;
}

G) Create dynamic page

Create [service].js in pages/services folder. Placing [service] in the filename is a special NextJS syntax. It is a placeholder for a route part. If the user types in localhost:3000/services/hello-world, then NextJS renders this page component. And what is more, the value ‘hello-world’ is available for use within the page component or functions in the [service].js file. By functions, I refer to special functions like getStaticProps() function.

The folder structure in our NextJS app should look like below:

Folder structure in our NextJS app
Folder structure in our NextJS app

Copy the following code and paste it in [service].js file:

import { Converter } from 'showdown';
import fs from 'fs';

export default function Service(props) {
    const {
        content,
        meta: { title },
    } = props;
    return (
        <div className="container">
            <h1>{title}</h1>
            <div dangerouslySetInnerHTML={{ __html: content }} />
        </div>
    );
}

export function getStaticProps(context) {
    const { service } = context.params;
    let content = fs.readFileSync(`services/${service}.md`, 'utf8');
    const converter = new Converter({ metadata: true });
    content = converter.makeHtml(content);
    const meta = converter.getMetadata();
    return { props: { content, meta } };
}

export function getStaticPaths() {
    const files = fs.readdirSync('services');
    const paths = files.map((file) => {
        const service = file.slice(0, file.indexOf('.md'));
        return { params: { service } };
    });
    return {
        paths,
        fallback: false,
    };
}

The file exports the page component. In addition, it exports the “getStaticProps” function as well as the “getStaticPaths” function. We already know the getStaticProps function. NextJS attaches props to the page component at build time. But, what is this “getStaticPaths” function.

Explaining getStaticPaths and getStaticProps

The “getStaticPaths” function also runs at build time. And it tells NextJS the actual pages that it has to create. For our app, we need only four pages corresponding to each of the markdown file. The pages are:

  • /services/gatsby-app
  • /services/nextjs-app
  • /services/react-app
  • /services/react-native-app

By giving a fallback value of false, we tell NextJS that we don’t need to create another more service pages. If the user types in /services/hello-world, then we redirect them to the 400 or Not found page.

Let’s examine the getStaticProps function in the service page a bit closely. We get the service param using the context object like so:

const { service } = context.params;

This is the the dynamic part of the route. We then read the corresponding markdown file. And store the title and content as props in the service page component. The service page component shows the title prop in a h1 tag. And it shows the content as inner HTML of a div element using the following code:

<div dangerouslySetInnerHTML={{ __html: content }} />

Now, let’s examine the getStaticPaths function. This function loops through each file in the services folder. It extracts the filename without extension and sets it as a page param.

const paths = files.map((file) => {
    const service = file.slice(0, file.indexOf('.md'));
    return { params: { service } };
});

When NextJS gets the paths array, it creates one page for each item in the paths array. To create the page, it uses the service param.

This is a bit difficult topic to understand. It is because NextJS triggers these functions as part of the build process. But once you understand and use it, you will unleash the full value of NextJS.

H) Head component

Head component is another NextJS component that allows us to place content in the head tag of a HTML page. It is convenient because we can place Head component in any component – page component, layout component or even other sub-components.

Each page needs a page title. So, open up pages/index.js and import the Head component.

import Head from 'next/head';

Use the Head component within the page component to set the page title like so:

<div className={styles.home}>
    <Head>
        <title>Vijay Consulting Services</title>
    </Head>
    ...
</div>

We have to set the title in pages/services/index.js (services page) and pages/services/[service].js (service page) as well. To create a title in service page, add the following code:

<Head>
    <title>{title}</title>
</Head>

As you can see from the code above, since we use the Head component from the page component, we have access to the title prop. And the title in the service page is dynamic.

We can use the Head component to set meta tags as well. For example, let us set a meta tag in the Layout component.

<Head>
    <meta name="viewport" content="initial-scale=1.0, width=device-width" />
</Head>

Since we set the meta tag in the Layout component, all pages in our app have this meta tag.

I) Static site generation

When we run the ‘next build’ command, we create the build output in the “.next” folder. This has a client part and a server part. So, we should deploy this code in our environments that support NodeJS. Both Netlify and Vercel are good candidates to host Node servers. But, we don’t need a Node server. So, we should opt for generating a static site. A static site has pre-built HTML pages along with some CSS and JavaScript. To generate a static site, use the ‘next export’ command after doing a ‘next build’. This command produces an out folder. We can deploy the contents of this out folder anywhere: Netlify, S3 buckets etc.

Out folder of a NextJS app
Out folder

To deploy the app as static site with Netlify, point Netlify to the github repo. Make sure to update the build command to “next build && next export”. And specify the output folder as “out” instead of “.next”. With these settings, we are all set to deploy our app in Netlify.

Just push some code to the Master branch. Netlify will pick up the latest code and trigger the build process. After build completes successfully, Netlify will host the out folder as a static site.


The code for the tutorial is available in a Github repo. View the sample app in Netlify. Please ask me any questions in the comments below.

Related Posts

2 thoughts on “Static website using NextJS and Markdown

    1. Sorry about the late reply. I have updated the article if you want to take a look. I was a bit busy on Aug, 2019. And forgot to reply to your comment.

Leave a Reply

Your email address will not be published.