A simple, configurable static blog system for Express.js applications. This library allows you to quickly add a blog to your Express app using static HTML files with minimal configuration. It isn't intended to be a long term solution. More of a quick answer to get up some blogs on your site while you figure out your long-term blog strategies.
This is not an automated CMS tool. You will need to deploy and/or stop-and-start your service each time you add a blog.
- 📁 File-based blog posts (HTML files)
- 🚀 In-memory caching for fast performance
- 🎨 Clean separation - you own the templates
- 📅 Date-based URL routing
- 🔍 Built-in search functionality
- 📄 Pagination support
- 📡 RSS feed generation
- 🌐 JSON API endpoints
- ⚡ Zero database dependency
- Simple: No database setup, no complex configuration. Just HTML files.
- Fast: In-memory caching means your blog loads quickly.
- Flexible: Use your own templates and styling - this library just handles the blog logic.
- SEO-Friendly: Clean URLs, RSS feeds, and static content that search engines love.
- Developer-Friendly: Built for Express.js developers who want to add a blog without the bloat.
npm install express-simple-static-blog
const express = require('express');
const expressStaticBlog = require('express-simple-static-blog');
const app = express();
// Set up view engine and views directory
app.set('view engine', 'ejs');
app.set('views', './views');
// Initialize the blog system
const blog = expressStaticBlog({
blogsDir: './views/blogs', // Directory containing blog HTML files
routePrefix: '/blogs' // URL prefix for blog routes
});
// Mount the blog router
app.use(blog.router());
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
Important: With minimal configuration, you must:
- Create a
views/blogs.ejs
template file (note: default expects .ejs extension) - Your blog HTML files will be sent as-is (no template processing for detail pages)
This approach lets you use HTML blog files while maintaining consistent site layout.
app.js:
const express = require('express');
const expressStaticBlog = require('express-simple-static-blog');
const app = express();
app.set('view engine', 'ejs');
app.set('views', './views');
app.engine('.html', require('ejs').renderFile);
const blog = expressStaticBlog({
blogsDir: './views/blogs',
routePrefix: '/blogs',
listTemplate: 'blogs.html', // Use .html extension
contentFormat: 'body-only', // Extract body content only
detailTemplate: 'blog-detail.html' // Template for blog details
});
app.use(blog.router());
views/blogs.html:
<!DOCTYPE html>
<html>
<head>
<title>Blog</title>
</head>
<body>
<%- include('partials/navigation') %>
<h1>Blog Posts</h1>
<% blogs.forEach(blog => { %>
<article>
<h2><a href="/blogs/<%= blog.year %>-<%= blog.month %>-<%= blog.day %>">
<%= blog.title %>
</a></h2>
<time><%= blog.date %></time>
<p><%= blog.desc %></p>
</article>
<% }); %>
<%- include('partials/footer') %>
</body>
</html>
views/blog-detail.html:
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
</head>
<body>
<%- include('partials/navigation') %>
<%- renderedContent %>
<%- include('partials/footer') %>
</body>
</html>
views/blogs/2025-01-15-first-post.html:
<!DOCTYPE html>
<html>
<head>
<title>My First Post</title>
<!-- [date: 2025-01-15] -->
<!-- [desc: Introduction to my blog] -->
</head>
<body>
<h1>My First Post</h1>
<p>Welcome to my blog!</p>
</body>
</html>
app.js:
const blog = expressStaticBlog({
blogsDir: './content/blog',
contentFormat: 'markdown',
metadataFormat: 'front-matter',
listTemplate: 'blog/index',
detailTemplate: 'blog/post'
});
content/blog/2025-01-15-getting-started.md:
---
date: 2025-01-15
title: Getting Started with Node.js
description: A beginner's guide to Node.js
---
# Getting Started with Node.js
Node.js is a powerful JavaScript runtime...
## Installation
First, download Node.js from the official website...
If you want blog HTML files to be served directly without any template processing:
const blog = expressStaticBlog({
blogsDir: './blogs',
routePrefix: '/blog',
listTemplate: 'blog-list' // Only the list uses a template
// No contentFormat or detailTemplate specified
});
Blog posts are HTML files with metadata in the Title element and HTML comments for 'date' and 'desc':
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My First Blog Post</title>
<!-- [date: 2025-08-15] -->
<!-- [desc: This is a description of my blog post.] -->
</head>
<body>
<h1>My First Blog Post</h1>
<p>Your blog content goes here...</p>
</body>
</html>
const blog = expressStaticBlog({
// Required
blogsDir: './blogs', // Path to blog HTML files
// Optional
cache: true, // Enable in-memory caching (default: true)
dateFormat: 'YYYY-MM-DD', // URL date format (default: 'YYYY-MM-DD')
metadataFormat: 'html-comment', // Metadata format (default: 'html-comment')
routePrefix: '/blogs', // URL prefix (default: '/blogs')
sortOrder: 'desc', // Sort order: 'desc' or 'asc' (default: 'desc')
paginate: false, // Enable pagination (default: false)
perPage: 10, // Items per page if pagination is on (default: 10)
listTemplate: 'blogs', // Template name for blog list page (default: 'blogs')
contentFormat: 'full-html', // Content format: 'full-html', 'body-only', 'markdown' (default: 'full-html')
detailTemplate: null, // Template for blog detail pages (null uses default behavior)
// RSS Feed options
feedTitle: 'Blog RSS Feed',
feedDescription: 'Latest posts',
feedLanguage: 'en'
});
The blog system creates the following routes:
GET /blogs
- List all blog postsGET /blogs/:year-:month-:day
- View specific blog postGET /blogs/feed.rss
- RSS feedGET /blogs/api/posts
- JSON API for postsGET /blogs/api/posts/:year-:month-:day
- JSON API for specific post
// Get all blog posts
const allPosts = blog.getAll();
// Get blog by date
const post = blog.getByDate('2025-08-15');
// Get recent posts
const recentPosts = blog.getRecent(5);
// Get paginated posts
const page = blog.getPaginated(1, 10);
// Search posts
const results = blog.cache.search('keyword');
// Refresh cache manually
blog.refresh();
The library expects your Express app to have templates in its views directory. You can customize template names with the listTemplate
and detailTemplate
options.
<!DOCTYPE html>
<html>
<head>
<title>Blog</title>
</head>
<body>
<h1>Blog Posts</h1>
<% if (blogs && blogs.length > 0) { %>
<% blogs.forEach(blog => { %>
<article>
<h2><%= blog.title %></h2>
<time><%= blog.date %></time>
<p><%= blog.desc %></p>
<a href="/blogs/<%= blog.year %>-<%= blog.month %>-<%= blog.day %>">Read more</a>
</article>
<% }); %>
<% } else { %>
<p>No blog posts available.</p>
<% } %>
<% if (pagination) { %>
<!-- Add pagination controls here -->
<% } %>
</body>
</html>
The library passes these variables to your list template:
blogs
- Array of blog objectspagination
- Pagination data (if enabled)search
- Current search queryversion
- From app.locals.version
When using contentFormat
other than 'full-html', you can specify a detailTemplate
:
<!-- If using a layout system (e.g., express-ejs-layouts), just the content: -->
<article class="blog-post">
<h1><%= title %></h1>
<time datetime="<%= date %>"><%= date %></time>
<div class="content">
<%- renderedContent %>
</div>
</article>
<!-- Or if creating a full page template: -->
<!DOCTYPE html>
<html>
<head>
<title><%= title %> - My Blog</title>
<link rel="stylesheet" href="/css/blog.css">
</head>
<body>
<nav><!-- Your navigation --></nav>
<article class="blog-post">
<h1><%= title %></h1>
<time datetime="<%= date %>"><%= date %></time>
<div class="content">
<%- renderedContent %>
</div>
</article>
<footer><!-- Your footer --></footer>
</body>
</html>
Variables passed to detail template:
- All blog properties at root level (
title
,date
,desc
,renderedContent
, etc.) blog
- Blog object with all metadata includingrenderedContent
pageTitle
- Same as blog title, for layout compatibilityversion
- From app.locals.version
Traditional approach - blog files are complete HTML documents that are sent directly to the browser. No template required.
Extracts only the content within <body>
tags from HTML files and renders it through your detailTemplate
. Perfect for reusing existing HTML content with new layouts.
Use Markdown files for your blog content. Requires metadataFormat: 'front-matter'
for metadata.
const blog = expressStaticBlog({
blogsDir: './content/blog',
contentFormat: 'markdown',
metadataFormat: 'front-matter',
detailTemplate: 'blog/detail'
});
Example Markdown blog file:
---
date: 2025-01-20
title: My Blog Post
description: This is my blog post about Node.js
---
# Welcome to my blog
This is a paragraph with **bold** text and *italics*.
## Features
- Easy to write
- Clean syntax
- Full markdown support
You can use front matter format for metadata with any content format:
const blog = expressStaticBlog({
blogsDir: './blogs',
metadataFormat: 'front-matter'
});
---
date: 2025-08-15
title: My Blog Post
description: This is my blog post
tags: javascript, nodejs
---
# My Blog Post
Content goes here...
// Get all posts sorted by date
const posts = blog.getAll();
// Get posts from a specific year
const yearPosts = blog.cache.getByYear(2025);
// Get posts from a specific month
const monthPosts = blog.cache.getByYearMonth(2025, 8);
// Get cache statistics
const stats = blog.cache.getStats();
// Add custom middleware before blog routes
app.use('/blogs', (req, res, next) => {
// Custom logic here
next();
}, blog.router());
If you have existing HTML blog files:
- Move blog files to a dedicated directory (e.g.,
./views/blogs/
) - Add metadata to each file using HTML comments:
<!-- [date: 2025-01-15] --> <!-- [desc: Blog post description] -->
- Create templates for list and detail pages
- Configure the library:
// Before: Manual blog routes
app.get('/blogs', (req, res) => {
res.render('blog-list', { posts: getPostsSomehow() });
});
app.get('/blogs/:slug', (req, res) => {
res.render('blog-post', { post: getPostSomehow(req.params.slug) });
});
// After: Using express-simple-static-blog
const blog = expressStaticBlog({
blogsDir: './views/blogs',
routePrefix: '/blogs',
listTemplate: 'blog-list.html',
contentFormat: 'body-only', // If your HTML uses includes
detailTemplate: 'blog-post.html'
});
app.use(blog.router());
If your blog HTML files use EJS includes (like <%- include('header') %>
):
const blog = expressStaticBlog({
blogsDir: './views/blogs',
routePrefix: '/blogs',
listTemplate: 'blogs.html',
contentFormat: 'body-only', // IMPORTANT: Extracts body content
detailTemplate: 'blog-detail.html' // REQUIRED: For processing includes
});
This configuration ensures:
- Your existing includes continue to work
- Blog content is wrapped in your site's layout
- All EJS features remain available
- Caching: Keep caching enabled for production
- File Count: The library handles hundreds of blog posts efficiently
- Templates: Use custom templates to match your site design
- Static Assets: Serve blog images/assets separately
The library includes built-in error handling:
app.use((err, req, res, next) => {
if (err.status === 404) {
res.status(404).send('Blog post not found');
} else {
res.status(500).send('Server error');
}
});
Problem: Express can't find your blog list template.
Solution:
// If using .html files:
listTemplate: 'blogs.html' // Include the extension
// If using .ejs files:
listTemplate: 'blogs' // No extension needed
Problem: Your blog posts have <%- include() %>
statements that aren't being processed.
Solution: Use the template-based approach:
const blog = expressStaticBlog({
blogsDir: './views/blogs',
contentFormat: 'body-only', // Extract body content
detailTemplate: 'blog-detail' // Use a template for rendering
});
Problem: Express doesn't know how to render .html files.
Solution: Configure Express to use EJS for .html files:
app.engine('.html', require('ejs').renderFile);
Problem: You see HTML tags in your blog posts instead of rendered content.
Solution: Use <%- %>
instead of <%= %>
in your templates:
<%- blog.content %> <!-- Renders HTML -->
<%= blog.content %> <!-- Escapes HTML (shows tags) -->
Blog posts not showing up?
- Ensure your HTML files have the correct metadata format
- Check that the
blogsDir
path is correct - Verify file permissions allow reading
Templates not rendering?
- Make sure you have a template file matching your
listTemplate
option - If using
.html
extension, specify it:listTemplate: 'blogs.html'
- Check that your view engine is properly configured
- For
.html
files with EJS:app.engine('.html', require('ejs').renderFile)
- Ensure EJS is installed as a peer dependency
EJS includes not working in blog posts?
- Use
contentFormat: 'body-only'
to extract body content - Specify a
detailTemplate
to process blog content through EJS - This allows your blog posts to use your site's layout and includes
Performance issues?
- Enable caching (it's on by default)
- Consider paginating if you have many posts
- Use the
getRecent()
method for homepage displays
Date parsing errors?
- Use the ISO format (YYYY-MM-DD) for dates
- Ensure dates are in the correct metadata format
This library can be used in TypeScript projects, though type definitions are not yet included.
// Add module declaration to your project
declare module 'express-simple-static-blog';
// Import and use
import * as express from 'express';
import * as expressStaticBlog from 'express-simple-static-blog';
const app = express();
const blog = expressStaticBlog({
blogsDir: './views/blogs',
routePrefix: '/blogs',
listTemplate: 'blogs',
contentFormat: 'markdown',
detailTemplate: 'blog-detail'
});
app.use(blog.router());
If you prefer not to use module declarations:
// Use require with type assertion
const expressStaticBlog = require('express-simple-static-blog') as any;
// Or with a basic type
interface BlogSystem {
router: () => express.Router;
getAll: () => any[];
getByDate: (date: string) => any;
getRecent: (count: number) => any[];
refresh: () => void;
}
const blog = expressStaticBlog({
blogsDir: './views/blogs'
}) as BlogSystem;
Since type definitions are not yet provided, you won't get IntelliSense autocomplete for configuration options or methods. Refer to this documentation for available options and methods.
Type definitions may be added in a future release. If you need TypeScript support urgently, consider opening an issue or contributing type definitions.
Pull requests are welcome! For major changes, please open an issue first to discuss what you would like to change.
Please make sure to:
- Update tests as appropriate
- Follow the existing code style
- Update documentation if needed
MIT
For issues and feature requests, please use the GitHub issue tracker.