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: > Couldn't 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 <div>Welcome to the Next.js blog</div>
}
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.
![Screenshot of the homepage in the browser. The content says welcome to the next.js blog.](https://i0.wp.com/css-tricks.com/wp-content/uploads/2020/07/r2LmPyTM.png?resize=1390%2C816&ssl=1)
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.
![Screenshot of the 404 page. It says 404 This page could not be found.](https://i2.wp.com/css-tricks.com/wp-content/uploads/2020/07/4Ee3BuaM.png?fit=1024%2C645&ssl=1)
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
directory - A 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: '<p>Incididunt cupidatat eiusmod ...</p>'
}
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, <Header>
and <Footer>
, which will be used in the different layout components defined in the _layouts
directory.
// _includes/header.js
export default function Header() {
return <header><p>Blog | Powered by Next.js</p></header>
}
// _includes/footer.js
export default function Footer() {
return <footer><p>©2020 | Footer</p></footer>
}
Layouts
We have two layout components in the _layouts
directory. One is the <DefaultLayout
> which is the base layout on top of which every other layout component will be built.
// _layouts/default.js
import Head from 'next/head'
import Header from '@includes/header'
import Footer from '@includes/footer'
export default function DefaultLayout(props) {
return (
<main>
<Head>
<title>{props.title}</title>
<meta name='description' content={props.description}/>
</Head>
<Header/>
{props.children}
<Footer/>
</main>
)
}
The second layout is the <PostLayout>
component that will override the title defined in the <DefaultLayout>
with the post title and render the HTML of the post. It also includes a link back to the homepage.
// _layouts/post.js
import DefaultLayout from '@layouts/default'
import Head from 'next/head'
import Link from 'next/link'
export default function PostLayout(props) {
return (
<DefaultLayout>
<Head>
<title>{props.title}</title>
</Head>
<article>
<h1>{props.title}</h1>
<div dangerouslySetInnerHTML={{__html:props.content}}/>
<div><Link href='/'><a>Home</a></Link></div>
</article>
</DefaultLayout>
)
}
next/head
is a built-in component to append elements to the <head>
of the page. next/link
is a built-in component that handles client-side transitions between the routes defined in the pages directory.
Homepage
As part of the index page, aka homepage, we will list all the posts inside the _posts
directory. The list will contain the post title and the permalink to the individual post page. The index page will use the <DefaultLayout>
and we’ll import the config in the homepage to pass the title
and description
to the layout.
// pages/index.js
import DefaultLayout from '@layouts/default'
import Link from 'next/link'
import { getConfig, getAllPosts } from '@api'
export default function Blog(props) {
return (
<DefaultLayout title={props.title} description={props.description}>
<p>List of posts:</p>
<ul>
{props.posts.map(function(post, idx) {
return (
<li key={idx}>
<Link href={'/posts/'+post.slug}>
<a>{post.title}</a>
</Link>
</li>
)
})}
</ul>
</DefaultLayout>
)
}
export async function getStaticProps() {
const config = await getConfig()
const allPosts = await getAllPosts()
return {
props: {
posts: allPosts,
title: config.title,
description: config.description
}
}
}
getStaticProps
is called at the build time to pre-render pages by passing props
to the default component of the page. We use this function to fetch the list of all posts at build time and render the posts archive on the homepage.
![Screenshot of the homepage showing the page title, a list with two post titles, and the footer.](https://i0.wp.com/css-tricks.com/wp-content/uploads/2020/07/KJidSMgw.png?resize=1544%2C904&ssl=1)
Post page
This page will render the title and contents of the post for the slug
supplied as part of the context
. The post page will use the <PostLayout>
component.
// pages/posts/[slug].js
import PostLayout from '@layouts/post'
import { getPostBySlug, getAllPosts } from "@api"
export default function Post(props) {
return <PostLayout title={props.title} content={props.content}/>
}
export async function getStaticProps(context) {
return {
props: await getPostBySlug(context.params.slug)
}
}
export async function getStaticPaths() {
let paths = await getAllPosts()
paths = paths.map(post => ({
params: { slug:post.slug }
}));
return {
paths: paths,
fallback: false
}
}
If a page has dynamic routes, Next.js needs to know all the possible paths at build time. getStaticPaths
supplies the list of paths that has to be rendered to HTML at build time. The fallback property ensures that if you visit a route that does not exist in the list of paths, it will return a 404 page.
![Screenshot of the blog page showing a welcome header and a hello world blue above the footer.](https://i0.wp.com/css-tricks.com/wp-content/uploads/2020/07/AXQYtDL4.png?resize=1542%2C908&ssl=1)
Production ready
Add the following commands for build
and start
in package.json
, under the scripts
section and then run npm run build
followed by npm run start
to build the static blog and start the production server.
// package.json
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
}
The entire source code in this article is available on this GitHub repository. Feel free to clone it locally and play around with it. The repository also includes some basic placeholders to apply CSS to your blog.
Improvements
The blog, although functional, is perhaps too basic for most average cases. It would be nice to extend the framework or submit a patch to include some more features like:
- Pagination
- Syntax highlighting
- Categories and Tags for posts
- Styling
Overall, Next.js seems really very promising to build static websites, like a blog. Combined with its ability to export static HTML, we can built a truly standalone app without the need of a server!
It’s a great tool to build your next website. It has many great features and advantages, which can make Nextjs your first option for building your next web application.
And this is a great blog for the Software engineers. They love to experiment with ideas with React atom symbol and Node. Next.js is an awesome new framework for building universal React applications. Thanks for sharing such a great article.
Hi I’m following this tutorial. I’ve gotten right to the end, unfortunately I can’t get my HTML tags to properly render as an element, I see
and for example in my blog posts. All the code is essentially the same except I’m not using the YAML. Thanks
I’d like to use this, but not sure what the path to the. “…js file” should be. I used npm to install. the console throws an error that the path to the .js file is wrong..
I ran into an issue with the getAllPosts function, I think there may be an issue here. I couldn’t use require.context since the function is running on the server-side. I used readdir instead, here’s my solution:
your code still run server side, because you can’t use fs on client side. The solution above works may you have to add @types/webpack-env
Please, help.
Error: Cannot find module ‘./osts/style-guide-101.md’
api\index.js (10:24) @ async getAllPosts
8 | for(const key of context.keys()){
9 | const post = key.slice(2);