Nextjs + Markdown + Content layer

How to build the static blog with a Content layer, markdown, and nextjs?

Build an entire static blog with markdown and content layer— full support of Home, Category, tag, search, SEO, sitemap and pagination. #series 2

Rajdeep Singh
FrontEnd web

--

Create a static blog with a Content layer, markdown, and next.
Create a static blog with a Content layer, markdown, and next.

The content layer is a new javascript content management library. The content layer help organize your markdown file.

Benefits of the content layer

  1. Automatic type definition
  2. Built-in validations
  3. Caching and incremental regeneration
  4. Live reloading
  5. Clean code and better readability

Steps

  1. Project Setup
  2. Project Structure
  3. Posts And Images Dummy data
  4. Pages
  5. Layout
  6. Home Page
  7. Reading Page
  8. Category Page
  9. Tag Page
  10. Search Page and Functionality
  11. SEO
  12. Sitemap
  13. pagination

Note:

My project builds with javascript, and I’m not using typescript in my content layer.

Project Setup

The happy news with the content layer npm package. With the content layer, you use fewer third-party packages to start working with markdown files.

npm install -D contentlayer next-contentlayer

Project Structure

The folder structure in contentlayer with nextjs.
The folder structure in the content layer with nextjs.

Posts And Images Dummy data

I provide dummy blog and image data for your study. the markdown file present in posts folder and images in public/images folder.

Pages

In the pages folder, all pages are present.

  1. pages/index.js Home page
  2. pages/blog/[slug].js reading page for our blog.
  3. pages/category/[slug].js Category page.
  4. pages/tag/[slug].js Tag pages.
  5. pages/Search.js Search pages.

Layout

Define the layout for our blog in pages/_app.js file.

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 first step is to import two packages from contentlayer .

import { allPosts } from "contentlayer/generated";import { pick } from "@contentlayer/client";

Fetch all posts using the getStaticProps in contentlayer/generated .

  1. contentlayer/generated Provide all the posts.
  2. @contentlayer/client Help to filter post data.
// fetch all postsexport async function getStaticProps() {
// all postsconst posts = allPosts.map((post) => pick(post, ["title", "date", "slug", "description", "summary", "draft", "image", "images", "tags", "categories"]));// filter publish postsconst publish= posts.filter( (post, i) =>{ return post.draft===false })return { props: { posts: publish.sort(sortByDate), }, }}

Reading page

The reading component helps to read the full article on your blog. Reading page path is pages/blog/[slug].js .

Import all the posts from contentlayer/generated

import { allPosts } from "contentlayer/generated"

Generate all paths using getStaticPaths nextjs function.

export async function getStaticPaths() {//  filter the post and get the publish post.const posts= allPosts.filter((post) =>{ return post.draft===false })// get all the post slugconst publish = posts.map((post) => ({ params: { slug: post.slug } }))return { paths: publish, fallback: false, }}

Reading the single post using the getStaticPropsand returning the UI component.

export async function getStaticProps({ params: { slug } }) {// fetch a single post by slugconst post = allPosts.find((post) => {     return  post.slug === slug})return { props: { post } }}

Category Page

The category page fetches all posts base on the category name. The category path is pages/category/[slug].js .

Import all the posts from contentlayer/generated

import { allPosts } from "contentlayer/generated"

Generate all category paths using getStaticPaths nextjs function.

export async function getStaticPaths() {let paths =[]
// get all category paths
allPosts.map( post => { if (post.draft===false){ post.categories.map( category=> { const slug = slugify(category) paths.push({ params: { slug } }) } ) } })return { paths, fallback: false, }}

Fetch all posts base on category slug using the getStaticPropsand returning the UI component.

export async function getStaticProps({ params: { slug } }) {let posts =[]
// get all category posts base on slugconst post= allPosts.map( (post) => { if (post.draft===false){ post.categories.filter( category => { const categorySlug = slugify(category) if(categorySlug === slug ){ posts.push(post) } })return posts } })return { props: { posts} }}

UI component for the category.

export default function Category({ posts }) {return (        <div className="container my-3">             <div className="row">                 <div className="col-lg-10 post-date m-1 p-2m-auto">      {          posts.map((post, index) => {                 return <ItemPost key={index} post={post} />           })     }</div> </div> </div>)}

Tag Page

The tag page fetches all posts base on the tag name. The tag path is pages/tag/[slug].js .

Import all the posts from contentlayer/generated

import { allPosts } from "contentlayer/generated"

Generate all tag paths using getStaticPaths nextjs function.

export async function getStaticPaths() {let paths =[]// get all tag paths allPosts.map(       post => {          if (post.draft===false){                 post.tags.map(                      tag=> {                           const  slug = slugify(tag)                           paths.push({ params: { slug } })                   }               )          }     })return { paths, fallback: false, }}

Fetch all posts base on tag slug using the getStaticPropsand returning the UI component.

export async function getStaticProps({ params: { slug } }) {let posts =[]// get all tag posts base on slugconst post= allPosts.map(   (post) => {       if (post.draft===false){              post.tags.filter(                   tag => {                          const  tagSlug = slugify(tag)                          if(tagSlug === slug ){                              posts.push(post)                           }      })return posts}})return { props: { posts} }}

UI component

export default function Category({ posts }) {return (<div className="container my-3"><div className="row"><div className="col-lg-10 post-date m-1 p-2m-auto">{posts.map((post, index) => {return <ItemPost key={index} post={post} />})}</div> </div> </div>)}

Search Page and Functionality

We add search functionality to the static blogs. With my step, you add successfully add search functionality to your blog.

Steps

  1. Create Search.json file
  2. Using Search.json file

Create Search.json file

Create search.json file using all post markdown data in your blog root level.

// serach functionaltyconst jsonString = JSON.stringify(publish)fs.writeFileSync('./search.json', jsonString, err => {  if (err) {     console.log('Error writing file', err)   } else {     console.log('Successfully wrote file')  }})

I’m create serach.json file on pages/index.js

// fetch all postsexport async function getStaticProps() {const posts = allPosts.map((post) => pick(post, ["title", "date", "slug", "description", "summary", "draft", "image", "images", "tags", "categories"]));// filter publish postsconst publish= posts.filter(       (post, i) =>{            return post.draft===false       })/*// serach functionalty// convert data into string*/const jsonString = JSON.stringify(publish)
// create serach.json file in root levelfs.writeFileSync('./search.json', jsonString, err => { if (err) { console.log('Error writing file', err) } else { console.log('Successfully wrote file') }}) return { props: { posts: publish.sort(sortByDate),},}}

Using Search.json file

We use the search.json file on pages/serach.json the page.

export default function Search() {const { query } = useRouter()const TempPosts = [] search.map(        (post) => {            if (post.draft === false) {            if (post.title.toLowerCase().includes(query.q) ||     post.summary.toLowerCase().includes(query.q) || post.description.toLowerCase().includes(query.q)) {                TempPosts.push(post)           } else {             TempPosts.push(null)  }}})//   remove null in postsconst posts = TempPosts.filter(      path => {        return path && path    })return (<div>   <Banner />    <div className="container">       <div className="row">          <div className="col-lg-8 m-auto">              {                posts.length > 0 ? posts.map((post, index) => (                   <Post key={index} post={post} />                     )) : <div className='m-auto p-5 mx-5 '>                  <h2 className='text-center'>                  {   
query.q ? `No post find base on ${query.q}` : 'loadding.. '

}
</h2>
</div>}</div> </div></div> </div>)}

Conclusion

We create a static site blog for the Home, Category, Tag, and Search components in the article.

We create an SEO, sitemap, and pagination for the static blog in the next article.

Any feedback and feature requests. plz, tell me in the comment section.

Thanks

You can follow and read more articles on officialrajdeepsingh.dev and tag them on Twitter.

Read More content on the Nextjs. Sign up for a free newsletter and join the frontend web community on medium.

--

--

Rajdeep Singh
FrontEnd web

Follow me if you learn more about JavaScript | TypeScript | React.js | Next.js | Linux | NixOS | https://linktr.ee/officialrajdeepsingh