A lightweight, file-based CMS designed for serving markdown content with versioning support, perfect for fan fiction and content that evolves over time.
- Markdown-based content with YAML front matter
- Version control for pages (v1, v2, v3, etc.)
- HTTPS-only secure serving
- Admin dashboard with visitor analytics
- Theme system with multiple CSS themes
- Concurrent-safe analytics with batched writes
- Static asset serving for images and media
- Docker-ready with minimal footprint
- Zero database - everything stored in files
npm installFor development, create self-signed certificates:
mkdir ssl
openssl req -x509 -newkey rsa:4096 -keyout ssl/key.pem -out ssl/cert.pem -days 365 -nodesFor production, use real certificates from Let's Encrypt or your certificate authority.
Copy the example configuration and edit it:
cp config.json.example config.jsonThen edit config.json:
{
"port": 3000,
"host": "0.0.0.0",
"admin": {
"password": "your-secure-password-here",
"path": "/admin"
},
"ssl": {
"cert": "./ssl/cert.pem",
"key": "./ssl/key.pem"
},
"defaultTheme": "default"
}Create a file content/hello.md:
---
title: "Hello World"
theme: "default"
description: "My first page"
---
# Hello World
This is my first page on MDCMS!
## Features
- Easy markdown editing
- Version support
- Beautiful themesnpm startVisit https://localhost:3000/hello to see your page!
Pages are markdown files in the content/ directory:
content/about.md→https://yoursite.com/aboutcontent/my-story.md→https://yoursite.com/my-story
Create multiple versions of the same page:
content/my-story.md→ Version 1 (original)content/my-story.v2.md→ Version 2content/my-story.v3.md→ Version 3
URL Access:
/my-story→ Latest version (automatic)/my-story/v1→ Specific version 1/my-story/v2→ Specific version 2
Version Navigation:
- Version links always use explicit routes (
/story/v1,/story/v2, etc.) - Clicking "v1" will take you to
/story/v1, not the base URL - Latest version shows "(latest)" label but still uses explicit route
---
title: "Page Title" # Optional, defaults to filename
theme: "dark" # Optional: default, dark, minimal
description: "SEO description"
published: true # Optional, defaults to true
public-versions: true # Optional, show version navigation (defaults to true)
date: "2025-01-15" # Optional
author: "Author Name" # Optional
tags: ["fiction", "drama"] # Optional
---public-versions: true(default) - Shows version navigation linkspublic-versions: false- Hides version navigation, useful for drafts or private versions
Create a directory for pages with assets:
content/
├── my-story.md # Version 1
├── my-story.v2.md # Version 2
└── my-story/ # Assets directory
├── cover.jpg # Shared across versions
├── chapter1.png # Version-specific
└── assets/
└── diagram.svg
Reference images in markdown:

MDCMS also supports Obsidian-style image embedding for easier content migration:
![['my-story/cover.jpg']]
![['assets/diagram.png']]These will be automatically converted to standard markdown image syntax with proper URL encoding for spaces and special characters.
- Go to
https://yoursite.com/admin - Enter the password from
config.json - View analytics and manage content
- Visitor Statistics: Total, daily, weekly, monthly visits
- Popular Pages: Most visited content with version breakdown
- Recent Activity: Latest visitor information
- Page Management: List all pages with version hierarchy
- URL Copying: One-click copy of page URLs (always latest version)
All visitor data is stored in analytics.json with:
- Timestamp and page visited
- Version accessed
- Anonymized visitor info (hashed IP)
- Referrer information
Four built-in themes are available:
- default - Clean, GitHub-inspired design
- dark - Dark mode with syntax highlighting
- minimal - Typography-focused, minimal design
- ember-sanctum - Rich, warm theme with ember-like styling
Set theme in front matter:
---
theme: "dark"
---Or set a global default in config.json:
{
"defaultTheme": "minimal"
}Create new CSS files in themes/ directory:
/* themes/custom.css */
body {
/* Your custom styles */
}Reference in front matter:
---
theme: "custom"
---Create Dockerfile:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]Create docker-compose.yml:
version: '3.8'
services:
mdcms:
build: .
ports:
- "3000:3000" # Change to "443:3000" for production HTTPS
volumes:
# Content and themes directories
- ./content:/app/content
- ./themes:/app/themes
# Configuration files (create from examples)
- ./config.json:/app/config.json
- ./analytics.json:/app/analytics.json
# SSL certificates
- ./ssl:/app/ssl
environment:
- NODE_ENV=production
restart: unless-stopped
container_name: mdcms-app
# Health check to ensure the service is running
healthcheck:
test: ["CMD", "node", "-e", "require('https').get('https://localhost:3000/', {rejectUnauthorized: false}, (res) => process.exit(res.statusCode === 404 ? 0 : 1)).on('error', () => process.exit(1))"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10sBefore running Docker:
# Copy configuration template
cp config.json.example config.json
# Edit config.json with your settings
# Ensure SSL certificates exist in ssl/ directoryRun with:
docker-compose up -d- Install Node.js 18+ on your server
- Copy all files to your server
- Install dependencies:
npm install --production - Configure SSL certificates
- Update
config.jsonwith production settings - Run with a process manager like PM2:
npm install -g pm2
pm2 start server.js --name mdcms
pm2 save
pm2 startupmdcms/
├── server.js # Main application
├── package.json # Dependencies
├── config.json # Configuration
├── analytics.json # Visitor data (auto-created)
├── CLAUDE.md # Implementation notes
├── README.md # This file
├── content/ # Your markdown content
│ ├── page.md # Single version page
│ ├── story.md # Story version 1
│ ├── story.v2.md # Story version 2
│ └── story/ # Story assets
│ └── image.jpg
├── themes/ # CSS themes
│ ├── default.css
│ ├── dark.css
│ ├── minimal.css
│ └── ember-sanctum.css
└── ssl/ # HTTPS certificates
├── cert.pem
└── key.pem
{
"port": 3000, // Server port
"host": "0.0.0.0", // Bind address
"admin": {
"password": "secure-password", // Admin login password
"path": "/admin" // Admin URL path
},
"ssl": {
"cert": "./ssl/cert.pem", // SSL certificate path
"key": "./ssl/key.pem" // SSL private key path
},
"defaultTheme": "default" // Default theme name
}- HTTPS-only - No HTTP support
- Secure sessions - HTTPOnly, Secure cookies
- Session timeout - 1 hour automatic expiry
- File restrictions - No direct access to .md or .json files
- Path validation - Prevents directory traversal
- Anonymous analytics - IP addresses are hashed
- Concurrent-safe analytics with batched writes every 10 seconds
- Atomic file operations prevent data corruption
- Static asset caching with proper headers
- Minimal dependencies for small footprint
- Graceful shutdown ensures no data loss
# Generate self-signed certificates
mkdir ssl
openssl req -x509 -newkey rsa:4096 -keyout ssl/key.pem -out ssl/cert.pem -days 365 -nodesEnsure the application has read/write permissions:
chmod 755 content/
chmod 644 content/*.md
chmod 666 analytics.jsonChange the port in config.json or find the conflicting process:
lsof -i :3000Check file permissions and disk space:
ls -la analytics.json
df -hThe codebase is organized into classes:
Analytics- Visitor tracking and statisticsContentManager- Page and version managementServer- HTTP server and routing
Create test content and verify:
- Page rendering works correctly
- Versioning system functions
- Admin authentication works
- Analytics data is collected
- Themes apply properly
MIT License - Feel free to use and modify for your projects.
- Check
CLAUDE.mdfor implementation details - Review server logs for error messages
- Ensure all file paths and permissions are correct
- Verify SSL certificates are valid and accessible