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:
- EJS, to template our application
- Express, the web framework for Node.js
- gray-matter, to parse the front matter from the Markdown files
- 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.