NextJS + Markdown + Blog
Build the static blog with Next.js and Markdown
Build an entire static blog with markdown—full support of sitemap, category or tags, and SEO support. #Series One
I start the entire static blog series. Today I start my first article on the series. First, I build a Static site generation (SSG) demo website with all functionality like SEO, sitemap, category, tags, and RSS page in the series.
All the codes are available on GitHub Repo. You can also check the final website demo with vercel.
Content
- Project Setup
- Project Structure
- Posts And Images Dummy data
- Pages
- Layout
- Home Page
- Reading page
- Category Page
- Tag Page
- Search Page
- SEO
Note
In the next article, we create a Category, tag and search page. In this article, we set up and build the website layout, home and Reading page for a website..
Project Setup
In our project, we need only four NPM packages.
Gray-matter and marked npm package work with markdown files and next-sitemap and next-seo help for website index and SEO.
Project Structure
For starting time, you need a basic setup for your project. Firstly install the nextjs with the latest version.
The second most important is folder structure. So I describe the folder structure and NPM package File.
.
├── components
│ ├── Banner.js
│ ├── Footer.js
│ ├── Header.js
│ ├── ItemPost.js
│ ├── Post.js
│ └── Sidebar.js
├── next.config.js
├── next-seo.config.js
├── next-sitemap.js
├── package.json
├── package-lock.json
├── pages
│ ├── _app.js
│ ├── blog
│ │ └── [slug].js
│ ├── category
│ │ └── [slug].js
│ ├── index.js
│ ├── Search.js
│ └── tag
│ └── [slug].js
├── posts
│ ├── how-is-npm-install-command.md
│ ├── how-to-add-css-in-next-js.md
│ ├── how-to-add-search-bar-functionality-in-ghost-cms-help-of-searchinghost.md
│ ├── how-to-capture-screenshots-in-raspberry-pi-4.md
│ ├── how-to-check-the-snap-store-package-available-for-raspberry-pi-4-or-not.md
│ ├── html-version-history.md
│ ├── keyboard-shortcut-keys-for-linux-terminal.md
│ ├── npm-outdated-command.md
│ ├── text-highlighting-in-html-5.md
│ ├── title-tag-in-html-5.md
│ └── what-is-next-js.md
├── public
│ ├── banner.png
│ ├── favicon.ico
│ ├── imagesnext-seo
│ │ ├── firstway-2.png
│ │ ├── firstway.png
│ │ ├── geenome.png
│ │ ├── gnome-screenshots.png
│ │ ├── How-to-capture-screenshots-in-Raspberry-PI-4.png
│ │ ├── HTML-Version-History.jpg
│ │ ├── Linux-Basic-Introduction--1-.png
│ │ ├── next.js-add-css-code.jpg
│ │ ├── next.js.png
│ │ ├── npm-commands.png
│ │ ├── npm-init-command-1.png
│ │ ├── secondway.png
│ │ ├── Text-Highlighting-In-HTML-5.png
│ │ ├── the-snap-store.png
│ │ └── Title-tag-In-HTML-5.jpg
│ ├── robots.txt
│ ├── sitemap-0.xml
│ ├── sitemap.xml
│ └── vercel.svg
├── README.md
├── search.json
├── styles
│ └── globals.css
├── utils
│ └── index.js
└── yarn.lock10 directories, 54 files
The Most Important file and folders are components, posts, next-seo.config.js, next-sitemap.js, and search.json files in my folder structure.
Posts And Images Dummy data
I provide demo markdown articles and image data in the Github repo. You can use it.
Pages
We need five pages or a path for our blog website. The path is straightforward for a simple blog.
paths List
pages/index.js
Home Pagepages/blog/[slug].js
Reading Pagepages/category/[slug].js
Category Pagepages/tag/[slug].js
Tag Pagepages/search.js
Search Page
Layout
I’m building the basic layout with bootstrap, custom components, and nextjs _app.js
import '../styles/globals.css'import Head from "next/head";import Header from "../components/Header";import Footer from "../components/Footer";function MyApp({ Component, pageProps }) {return ( <><Head> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossOrigin="anonymous" /></Head> <Header/> <Component {...pageProps} /> <Footer /></>)}export default MyApp
Home Page
The home page is a specific page on a static website. In the home firstly import some library on-page.
import fs from 'fs'import path from 'path'import matter from 'gray-matter'
Then, we need nextjs getStaticProps
to help pass props into components in a second step.
First, we access files in the page folder.
// Get markdown files from the posts dirconst files = fs.readdirSync(path.join('posts'))
In the second step, we collect the post-frontmatter data and slug.
// Get slug and frontmatter from postsconst tempPosts = files.map((filename) => {
// Create slugconst slug = filename.replace('.md', '')// Get frontmatterconst markdownWithMeta = fs.readFileSync( path.join('posts', filename), 'utf-8')convert data in json formatconst { data: frontmatter } = matter(markdownWithMeta)// base frontmatter data we find the publish articles(draft : false)if (frontmatter.draft === false) { return { slug, frontmatter, }} else { return null}})
Filter the articles and remove null
value in tempPosts
variables
// when we remove draft article we receve a return null and now remove null in tempPosts []const posts = tempPosts.filter( post => { return post && post })
In the end, return the post data with props in the component. sortByDate
Function help to sort article based on date.
return { props: { posts: posts.sort(sortByDate), },}
Summaries all above the home page code in the down below section.
export async function getStaticProps() {// Get markdown files from the posts dirconst files = fs.readdirSync(path.join('posts'))
// Get slug and frontmatter from postsconst tempPosts = files.map((filename) => {// Create slugconst slug = filename.replace('.md', '')// Get frontmatterconst markdownWithMeta = fs.readFileSync( path.join('posts', filename), 'utf-8')const { data: frontmatter } = matter(markdownWithMeta)// base frontmatter data we find the publish articles(draft : false)if (frontmatter.draft === false) { return { slug, frontmatter, } } else { return null}})// when we remove draft article we receve a return null and now remove null in tempPostsconst posts = tempPosts.filter( post => { return post && post })return { props: { posts: posts.sort(sortByDate), }, }}
Home UI Design
export default function Home({ posts }) {return (<div> <Banner /> <div className="container"> <div className="row"> <div className="col-lg-8"> {posts.map((post, index) => ( <Post key={index} post={post} /> )} </div> <Sidebar /> </div> </div></div>)}
Reading page
Reading the page help to read the entire post. reading a file on the side blog/[slug].js
. We read the article base on the slug. We use two next.js functionality for a static generation, getStaticPaths, and getStaticProps.
getStaticPaths
: Generate all the pages on build time.getStaticProps
: Get the post data and pass the component.
For the reading page, first import the npm and node packages on top of the file.
import fs from 'fs'import path from 'path'import matter from 'gray-matter'
getStaticPaths
In getStaticPaths
help to generate all paths on build time. It is a simpler code that we discussed previously.
getStaticPaths
Help pass the path as props into components. So you can easily access all the slugs and blog data based on a slug.
First, we access files in the page folder.
// Get markdown files from the posts dirconst files = fs.readdirSync(path.join('posts'))
We collect the post-frontmatter data and slug only publish posts in the second step.
// Get slug and frontmatter from postsconst temppaths = files.map((filename) => {// Get frontmatterconst markdownWithMeta = fs.readFileSync(path.join('posts', filename),'utf-8')// convert data into json const { data: frontmatter } = matter(markdownWithMeta)// get all publish articles and pass slug into paramsif (frontmatter.draft === false) {return { params: { slug: filename.replace('.md', ''), }, }} else { return null}})
Filter the articles and remove null
value in tempPosts
variables
// when we remove draft article we receve a return null and now remove null in tempPosts []const posts = tempPosts.filter(post => { return post && post })
In the end, return all the path data with props.
return { paths, fallback: false,}
Summaries all getStaticPaths
Reading page code in the down below section.
export async function getStaticPaths() {// Get files from the posts dirconst files = fs.readdirSync(path.join('posts'))// Get slug and frontmatter from postsconst temppaths = files.map((filename) => {// Get frontmatterconst markdownWithMeta = fs.readFileSync(path.join('posts', filename),'utf-8')const { data: frontmatter } = matter(markdownWithMeta)if (frontmatter.draft === false) {return {params: {slug: filename.replace('.md', ''),},}} else {return null}})// remove null in tempPostsconst paths = temppaths.filter(path => {return path && path})return {paths,fallback: false,}}
getStaticProps
getStaticProps
help to access a single post and pass it into the component.
Firstly we get a post base on a slug. We use the join inbult method with the pass a slug to find a post. Based on a slug, we access a single post.
const markdownWithMeta = fs.readFileSync(path.join('posts', slug + '.md'),'utf-8')
In the second step, we convert data into json format using the gray-matter npm package.
const { data: frontmatter, content } = matter(markdownWithMeta)
In the last step, return the data.
return { props: { frontmatter, slug, content, },}
Summaries all the getStaticProps
reading page code in the down below section.
export async function getStaticProps({ params: { slug } }) {const markdownWithMeta = fs.readFileSync( path.join('posts', slug + '.md'), 'utf-8')const { data: frontmatter, content } = matter(markdownWithMeta)return { props: { frontmatter, slug, content, },}}
Reading Page UI Design
export default function PostPage({ content, frontmatter }) {const date = new Date(frontmatter.date)return (<><div className="container my-5"> <div className="row"> <div className="col-lg-10 m-auto"> <div className='card card-page'><a href={`/blog/${frontmatter.slug}`} > <img className="card-img-top" src={ImageUrl(frontmatter.image)} alt={frontmatter.title} /></a><h1 className='post-title mt-2 p-2'>{frontmatter.title}</h1><div className='post-date m-1 p-2'> <div><h6>{`${date.getMonth() + 1} - ${date.getDate()} - ${date.getFullYear()}`} </h6> </div><div> {frontmatter.categories.map(category => {const slug = slugify(category)return (<Link key={category} href={`/category/${slug}`}><a className='btn'><h6 className=' post-title'>#{category}</h6></a></Link>)})} </div></div><div className='post-body p-5 m-auto' dangerouslySetInnerHTML={{ __html: marked.parse(content) }}></div></div></div></div></div></>)}
In the next article we create catgory and tag page for our blog.
All the codes are available on GitHub Repo. You can also check the final website demo with vercel.
Conclusion
I know my article is very dull. Suppose you have a basic knowledge of nextjs. I suggest understanding the code and bookmarking the article. If you have any problem with it, then read my article.