Markdown blog with EJS

This post is about creating a Markdown blog with EJS.

Intro #

Writing a post content with HTML is not as easy as doing it with Markdown.
Since my coming back to the roots whit a static site, I write my posts content with Markdown.
A little time after that, I discovered Deta Cloud and was able to try everything I've learned about Node.js on a live server.
My favorite templating language is EJS, and I wanted to create a dead simple blog by combining the content of a Markdown file with an EJS template.
After a lot of research and tries & fails, I've understood the mechanism needed to accomplish my goal.
I found some tutorials about the subject and this tutorial is inspired by the last one I stumbled on, Building A Markdown Blog App with Express and EJS, with some improvements and all the details.

Requirements #

To execute our magic spell, we'll need the following packages :

  1. EJS, to template our application
  2. Express, the web framework for Node.js
  3. gray-matter, to parse the front matter from the Markdown files
  4. markdown-it, to parse the Markdown files content

To install them with one command :

npm i ejs express gray-matter markdown-it

Server setup #

In your main server file, mine is index.js, put the following :

/index.js
const express = require("express")
const app = express()

// Built-in module to access and interact with the file system
const fs = require("fs")
// To parse front matter from Markdown files
const matter = require("gray-matter")

app.set("view engine", "ejs")
app.use(express.static("public"))

const getPosts = () => {
// Get the posts from their directory
const posts = fs.readdirSync(__dirname + "/views/posts").filter((post) => post.endsWith(".md"))
// Set the post content as an empty array
const postContent = []
// Inject into the post content array the front matter
posts.forEach((post) => {
postContent.push(matter.read(__dirname + "/views/posts/" + post))
})

/**
* 1- Return a list of posts as a two dimensional array containing for each one :
* . the post filename with it's extension (e.g : postFilename.md)
* . the post content as an object {content:"Markdown content as a string", data:{front matter}, excerpt:""}
* 2- Return each array as an object and create a Date instance from it's date front matter
* 3- Sort posts by publication's date in descending order (newest to oldest)
*/

const postsList = posts
.map(function (post, i) {
return [post, postContent[i]]
})
.map((obj) => {
return { ...obj, date: new Date(obj[1].data.date) }
})
.sort((objA, objB) => Number(objB.date) - Number(objA.date))

return postsList
}

// Render the list of posts on the main route
app.get("/", (req, res) => {
res.render("postsList", {
posts: getPosts(),
})
})

// Using a route parameter to render each post on a route matching it's filename
app.get("/posts/:post", (req, res) => {
const postTitle = req.params.post // Get the Markdown filename

// Read the Markdown file and parse it's front matter
const post = matter.read(__dirname + "/views/posts/" + postTitle + ".md")

// Convert the Markdown file content to HTML with markdown-it
const md = require("markdown-it")({ html: true }) // Allows HTML tags inside the Markdown file
const content = post.content // Read the Markdown file content
const html = md.render(content) // Convert the Markdown file content to HTML

// Render the postsTemplate for each post and pass it's front matter as a data object into postsTemplate
res.render("postsTemplate", {
title: post.data.title,
date: post.data.date,
postContent: html,
})
})

// Launching the application on port 3000
app.listen(3000, () => {
console.log(`App 🚀 @ http://localhost:3000`)
})

As you can see everything is explained in detail.
Nota bene : I'm directly using the views folder as the template files location, no need to declare it, Express detects it by default, and the Markdown files are under the views folder inside another folder called posts.

I want to bring your attention to one particular point.
When we render the postTemplate and pass the Markdown front matter and content as a data object, we can add and pass as many key: value pair as we want, but we can't call an undefined key inside the postTemplate !
So, if you add a description: my post description to the front matter of the Markdown file, you can't call it directly inside the postTemplate without adding it in the data object.
Nota bene : No need to declare the .ejs extension for a template file, Express detects it by default.

Rendering the frontend #

As you have seen in index.js, I'm rendering a list of posts on the main route from a template called postsList.ejs. Add the following to this file :

<!-- /views/postsList.ejs -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Home | My blog</title>
</head>
<body>
<h1>
Welcome to my blog
<br />
List of recent posts
</h1>
<% posts.forEach(post => { %>
<!-- Get the Markdown filename without it's extension -->
<% const postFilename = post[0].replace(/\.[^/.]+$/, "") %>
<!-- Get the Markdown post title from it's front matter -->
<% const postTitle = post[1].data.title %>
<!-- Render the title as a link to the post -->
<h2><a href="/posts/<%= postFilename %>"><%= postTitle%></a></h2>
<% }) %>
</body>
</html>

Now, each post has the same structure, one template file called postsTemplate.ejs. Add the following to this one :

<!-- /views/postsTemplate.ejs -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%= title %> | My blog</title>
</head>
<body>
<h1><%= title %></h1>
<p><%= date %></p>
<div><%- postContent %></div>
</body>
</html>

Everything is in place, we can now write our posts with Markdown in the views folder under the posts folder 🥳

I've created two files for you to see the output if you try it :

---
title: My first article
date: 2022/07/23

---


This is the content of my first article

<!--- /views/posts/my-first-article.md -->
---
title: A second post
date: 2022/07/25

---


Here goes the content of my second post

<!--- /views/posts/a-second-post.md -->

The app structure looks like the following tree :

// App's structure without the node_modules folder
├── index.js
├── package-lock.json
├── package.json
└── views
├── posts
│ ├── a-second-post.md
│ └── my-first-article.md
├── postsList.ejs
└── postsTemplate.ejs

I hope that this tutorial will be helpful for everyone trying to create a Markdown blog with EJS.

SYA,
LebCit.