Create your Markdown blog in Node.js

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

  1. Create a folder called markdown-blog-nodejs
  2. Open it with your code editor. I use VSCode that natively supports Markdown
  3. Initialize your Node.js project: npm init -y
  4. 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:

  1. views where the HTML templates and the Markdown files will be stored
  2. 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:

  1. LiteNode splits the frontmatter from the content
  2. 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:

  1. index.html: the main HTML template file
  2. 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:

  1. The frontmatter has been assigned to fm, so to access docTitle and docDescription we append those frontmatter properties by fm., it's just like accessing an object properties with dot notation.
  2. The markdownContentParsedToHtmlWithMarked has been assigned to html_content. Any rendered variable that starts with html_ in LiteNode will return its raw value. In this case we are instructing LiteNode's render method to return the raw value of markdownContentParsedToHtmlWithMarked. 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:

  1. npm i: only once to install the dependencies
  2. npm 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 file
  • fileDir: The directory of the file relative to the base directory
  • fileName: The name of the file
  • fileBaseName: 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!

Note: The background image is a photo by Daniel Dan.

SYA,
LebCit.