This post is about the process of creating a Markdown blog in Node.js.
I wrote, in December 2023, an article about the same subject (Create a simple Markdown-based blog in Node.js), but this new one will show you how to achieve the same goal with a much simpler and effective approach. By the end of this tutorial, you'll learn how to generate a static site from your Markdown content.
I assume you already have an idea of what Markdown is good for, if you don't click the link it's a useful section to read.
Dependencies
To create your Markdown blog, you'll only need two dependencies: LiteNode and Marked.
Installation
- Create a folder called markdown-blog-nodejs
- Open it with your code editor. I use VSCode that natively supports Markdown
- Initialize your Node.js project:
npm init -y
- Install the dependencies:
npm i litenode marked
These steps will create a package.json file and install LiteNode and Marked in your project.
Starting your application
Before continuing, add "type": "module"
to your package.json file to enable the use of import
statements instead of require
.
Create at the root of your project 2 folders:
- views where the HTML templates and the Markdown files will be stored
- static that will hold the front-end styles, scripts, images...
Always at the root of your project, create the entry file index.js and populate it with:
import { LiteNode } from "litenode"
import { marked } from "marked" // for later
const app = new LiteNode()
app.get("/", (req, res) => {
res.txt("Welcome to the main route!")
})
app.startServer()
Start you application with node index
. Your app will respond by:
You have the latest version of litenode
App @ http://localhost:5000
Click on the link and your default browser will open and display: Welcome to the main route!
Rendering
LiteNode allows you to render HTML templates where you can load Markdown files contents. The Markdown files will be processed in two steps:
- LiteNode splits the
frontmatter
from thecontent
- Marked parses the
content
only. You can use any parser that suits your needs but I highly recommend Marked.
A practical example
Create in the views folder 2 files:
- index.html: the main HTML template file
- home.md: the Markdown file that holds the contents of the homepage
Add this HTML to index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="{{fm.docDescription}}" />
<title>Markdown App | {{fm.docTitle}}</title>
</head>
<body>
<h1>Markdown App</h1>
<main>{{html_content}}</main>
</body>
</html>
And this Markdown to home.md:
---
docTitle: "Home"
docDescription: "A Markdown application built with LiteNode and Marked in Node.js"
---
Welcome to my Markdown application!<br>
I can use HTML in a Markdown file if I want to.<br>
Markdown is simply awesome!
This is a new paragraph, no need for HTML markup.<br>
An [MD Cheat Sheet](https://litenode-markdown-app.pages.dev/tutorial/markdown-cheat-sheet/) for new Markdown users.
LiteNode retrieves dynamic variables from the `frontmatter` like `docTitle` and `docDescription`.<br>
I can do anything with Markdown using LiteNode because they are made for each other!
Update index.js by replacing the app.get...
block with:
app.get("/", (req, res) => {
// Start by separating the Markdown file "frontmatter" from its "content"
const parsedMarkdownFileByLiteNode = app.parseMarkdownFile("home.md")
// Get the "content" and the "frontmatter" from parsedMarkdownFileByLiteNode
const { content, frontmatter } = parsedMarkdownFileByLiteNode
// Convert the Markdown "content" to HTML with Marked
const markdownContentParsedToHtmlWithMarked = marked.parse(content)
// Render "index.html"
res.render("index.html", {
// assign "frontmatter" to "fm"
fm: frontmatter,
// assign "markdownContentParsedToHtmlWithMarked" to "html_content"
html_content: markdownContentParsedToHtmlWithMarked,
})
})
Since you have modified a server file, in this case index.js, you have to restart the server by clicking anywhere in your terminal to give it the focus and hold Ctrl+C
to stop the server. Now type node index
and click on the link or refresh your browser on the previous page. You can see that dynamic variables like {{fm.docTitle}}
are replaced with their corresponding values (e.g., 'Home') in the rendered index.html.
Some explanation
While the new logic of app.get...
in index.js is explained by the provided comments, I want to bring your attention on two particular aspects:
- The
frontmatter
has been assigned tofm
, so to accessdocTitle
anddocDescription
we append thosefrontmatter
properties byfm.
, it's just like accessing an object properties with dot notation. - The
markdownContentParsedToHtmlWithMarked
has been assigned tohtml_content
. Any rendered variable that starts withhtml_
in LiteNode will return its raw value. In this case we are instructing LiteNode'srender
method to return the raw value ofmarkdownContentParsedToHtmlWithMarked
. You can read more about it in the RAW HTML section of LiteNode's documentation.
A simple HTML blog
For this tutorial, we are going to use the Responsive Side Menu layout from https://pure-css.github.io/.
In order to facilitate this article I've created a GitHub repository that you can clone then launch by:
npm i
: only once to install the dependenciesnpm index
: to start/restart the application
I also added the logic to build a static site in the build folder and a command that you can run
to create your static site: npm run build
.
The code includes detailed comments explaining how both the blog index and individual posts are rendered.
The process of building a static site is similar to the rendering one except that we use another method called renderToFile
instead of render
.
Nota bene
Additional notes to help understand key aspects of the code:
The blog
Here is the code that renders the blog:
// Route for the blog view
app.get("/", async (req, res) => {
// Parse all the Markdown files in the posts folder with LiteNode
// Note that here we use parseMarkdownFileS for multiple fileS
const mdFiles = await app.parseMarkdownFileS("posts")
// Render "index.html" which is in the layouts folder
res.render("layouts/index.html", {
blog: true, // Set a true flag on blog
posts: mdFiles, // pass all the posts
})
})
Both methods parseMarkdownFileS and parseMarkdownFile return the content
and the frontmatter
but also:
filePath
: The full path to the filefileDir
: The directory of the file relative to the base directoryfileName
: The name of the filefileBaseName
: The base name of the file without its extension
This allowed us in posts-blog.html to:
<!--Wrap each post title in a link to the post-->
<a href="/{{fileBaseName}}"><h1>{{frontmatter.postTitle}}</h1></a>
You'll notice that I used in posts-blog.html the #set and #each directives as well as the sortBy and truncate filters.
A post content
In index.html inside the layouts folder you could do:
<!--Otherwise display the HTML post content-->
<div class="content">{{html_content}}</div>
<!--Instead of-->
<!--Otherwise we are on a single post view and we display the post content-->
<div class="content">{{#include("components/posts-content.html")}}</div>
This was intended to show you how to load a specific HTML file for a single post view because you'll surely extend it with more dynamic contents, styles and scripts.
The about page
In the build process, about.md doesn't have a dedicated route like in the rendering one. Looking at buildRoutes.js inside the build folder you'll notice a comment about this aspect:
// Note that "about.md" will be taken care of here so no need for a separate route for it!
const pagesFiles = await app.parseMarkdownFileS("pages")
const postsFiles = await app.parseMarkdownFileS("posts")
// Merge the pages and the posts arrays into a single array named mdFiles
const mdFiles = pagesFiles.concat(postsFiles)
Since the files inside the pages folder, where about.md is located, are parsed along with the files inside the posts folder then merged into a single array, we don't need a separate route to render about.md to an HTML file, it's taken care of right here.
Conclusion
This tutorial aims to show you how LiteNode simplifies and enhances the process of rendering and building a static site without the need of helper functions, lot of explanations and the use of four modules to do the work of two!
This tutorial covers just the basics of LiteNode's capabilities for rendering and building static sites, providing you with a foundation to explore more advanced features.
All you need is to take a look around LiteNode's documentation!