In this article, we will use Next.js to build a static blog framework with the design and structure inspired by Jekyll. I’ve always been a big fan of how Jekyll makes it easier for beginners to setup a blog and at the same time also provides a great degree of control over every aspect of the blog for the advanced users.
With the introduction of Next.js in recent years, combined with the popularity of React, there is a new avenue to explore for static blogs. Next.js makes it super easy to build static websites based on the file system itself with little to no configuration required.
The directory structure of a typical bare-bones Jekyll blog looks like this:
.
???? _posts/ …blog posts in markdown
???? _layouts/ …layouts for different pages
???? _includes/ …re-usable components
???? index.md …homepage
???? config.yml …blog config
The idea is to design our framework around this directory structure as much as possible so that it becomes easier to migrate a blog from Jekyll by simply reusing the posts and configs defined in the blog.
For those unfamiliar with Jekyll, it is a static site generator that can transform your plain text into static websites and blogs. Refer the quick start guide to get up and running with Jekyll.This article also assumes that you have a basic knowledge of React. If not, React’s getting started page is a good place to start.
Installation
Next.js is powered by React and written in Node.js. So we need to install npm first, before adding next, react and react-dom to the project.
mkdir nextjs-blog && cd $_
npm init -y
npm install next react react-dom –save
To run Next.js scripts on the command line, we have to add the next command to the scripts section of our package.json.
scripts: {
dev: next
}
We can now run npm run dev on the command line for the first time. Let’s see what happens.
$ npm run dev
> [email protected] dev /~user/nextjs-blog
> next
ready – started server on http://localhost:3000
Error: > Couldnt find a `pages` directory. Please create one under the project root
The compiler is complaining about a missing pages directory in the root of the project. We’ll learn about the concept of pages in the next section.
Concept of pages
Next.js is built around the concept of pages. Each page is a React component that can be of type .js or .jsx which is mapped to a route based on the filename. For example:
File Route
—- —–
/pages/about.js /about
/pages/projects/work1.js /projects/work1
/pages/index.js /
Let’s create the pages directory in the root of the project and populate our first page, index.js, with a basic React component.
// pages/index.js
export default function Blog() {
return
}
Run npm run dev once again to start the server and navigate to http://localhost:3000 in the browser to view your blog for the first time.
Out of the box, we get:
Hot reloading so we don’t have to refresh the browser for every code change.Static generation of all pages inside the /pages/** directory.Static file serving for assets living in the/public/** directory.404 error page.
Navigate to a random path on localhost to see the 404 page in action. If you need a custom 404 page, the Next.js docs have great information.
Dynamic pages
Pages with static routes are useful to build the homepage, about page, etc. However, to dynamically build all our posts, we will use the dynamic route capability of Next.js. For example:
File Route
—- —–
/pages/posts/[slug].js /posts/1
/posts/abc
/posts/hello-world
Any route, like /posts/1, /posts/abc, etc., will be matched by /posts/[slug].js and the slug parameter will be sent as a query parameter to the page. This is especially useful for our blog posts because we don’t want to create one file per post; instead we could dynamically pass the slug to render the corresponding post.
Anatomy of a blog
Now, since we understand the basic building blocks of Next.js, let’s define the anatomy of our blog.
.
?? api
? ?? index.js # fetch posts, load configs, parse .md files etc
?? _includes
? ?? footer.js # footer component
? ?? header.js # header component
?? _layouts
? ?? default.js # default layout for static pages like index, about
? ?? post.js # post layout inherts from the default layout
?? pages
? ?? index.js # homepage
| ?? posts # posts will be available on the route /posts/
| ?? [slug].js # dynamic page to build posts
?? _posts
?? welcome-to-nextjs.md
?? style-guide-101.md
Blog API
A basic blog framework needs two API functions:
A function to fetch the metadata of all the posts in _posts directoryA function to fetch a single post for a given slug with the complete HTML and metadata
Optionally, we would also like all the site’s configuration defined in config.yml to be available across all the components. So we need a function that will parse the YAML config into a native object.
Since, we would be dealing with a lot of non-JavaScript files, like Markdown (.md), YAML (.yml), etc, we’ll use the raw-loader library to load such files as strings to make it easier to process them.
npm install raw-loader –save-dev
Next we need to tell Next.js to use raw-loader when we import .md and .yml file formats by creating a next.config.js file in the root of the project (more info on that).
module.exports = {
target: serverless,
webpack: function (config) {
config.module.rules.push({test: /.md$/, use: raw-loader})
config.module.rules.push({test: /.yml$/, use: raw-loader})
return config
}
}
Next.js 9.4 introduced aliases for relative imports which helps clean up the import statement spaghetti caused by relative paths. To use aliases, create a jsconfig.json file in the project’s root directory specifying the base path and all the module aliases needed for the project.
{
compilerOptions: {
baseUrl: ./,
paths: {
@includes/*: [_includes/*],
@layouts/*: [_layouts/*],
@posts/*: [_posts/*],
@api: [api/index],
}
}
}
For example, this allows us to import our layouts by just using:
import DefaultLayout from @layouts/default
Fetch all the posts
This function will read all the Markdown files in the _posts directory, parse the front matter defined at the beginning of the post using gray-matter and return the array of metadata for all the posts.
// api/index.js
import matter from gray-matter
?
export async function getAllPosts() {
const context = require.context(../_posts, false, /.md$/)
const posts = []
for(const key of context.keys()){
const post = key.slice(2);
const content = await import(`../_posts/${post}`);
const meta = matter(content.default)
posts.push({
slug: post.replace(.md,),
title: meta.data.title
})
}
return posts;
}
A typical Markdown post looks like this:
—
title: Welcome to Next.js blog!
—
**Hello world**, this is my first Next.js blog post and it is written in Markdown.
I hope you like it!
The section outlined by — is called the front matter which holds the metadata of the post like, title, permalink, tags, etc. Here’s the output:
[
{ slug: style-guide-101, title: Style Guide 101 },
{ slug: welcome-to-nextjs, title: Welcome to Next.js blog! }
]
Make sure you install the gray-matter library from npm first using the command npm install gray-matter –save-dev.
Fetch a single post
For a given slug, this function will locate the file in the _posts directory, parse the Markdown with the marked library and return the output HTML with metadata.
// api/index.js
import matter from gray-matter
import marked from marked
?
export async function getPostBySlug(slug) {
const fileContent = await import(`../_posts/${slug}.md`)
const meta = matter(fileContent.default)
const content = marked(meta.content)
return {
title: meta.data.title,
content: content
}
}
Sample output:
{
title: Style Guide 101,
content:
Incididunt cupidatat eiusmod …
}
Make sure you install the marked library from npm first using the command npm install marked –save-dev.
Config
In order to re-use the Jekyll config for our Next.js blog, we’ll parse the YAML file using the js-yaml library and export this config so that it can be used across components.
// config.yml
title: Next.js blog
description: This blog is powered by Next.js
?
// api/index.js
import yaml from js-yaml
export async function getConfig() {
const config = await import(`../config.yml`)
return yaml.safeLoad(config.default)
}
Make sure you install js-yaml from npm first using the command npm install js-yaml –save-dev.
Includes
Our _includes directory contains two basic React components,