A flexible static blog / website generation engine.
npm install blorgblorg config.jsonWhere config.json defines your blog structure (see Configuration below).
A typical blog setup:
my-blog/
├── build.js # Your build script
├── posts/ # Markdown posts
│ ├── my-first-post.md
│ └── another-post.md
├── templates/ # Nunjucks templates
│ ├── post.html # Individual post template
│ ├── index.html # Index/archive template
│ └── feed.xml # Atom feed template
└── output/ # Generated site (git submodule or deploy target)
Posts are markdown files with a JSON metadata header:
{
"title": "My First Post",
"date": "2026-01-24",
"author": "Your Name"
}
Your markdown content here. Supports **GFM** and fenced code blocks
with syntax highlighting.| Field | Required | Description |
|---|---|---|
title |
Yes | Post title |
date |
Yes | Publication date (used for sorting) |
author |
No | Author name |
draft |
No | Set to true to exclude from output |
path |
No | Custom URL path (overrides config pattern) |
base |
No | URL slug (auto-generated from title if omitted) |
Any additional fields you add are available in templates via post.spec.yourField.
By default, posts use the path pattern from config (e.g., /{year}/{month}/{title}.html). Override per-post:
{
"title": "My Post",
"date": "2026-01-24",
"path": "/{title}.html"
}Path patterns support: {year}, {month}, {title} (the base slug).
Blorg uses Nunjucks for templating.
Renders individual posts. Available variables:
| Variable | Description |
|---|---|
spec |
Post metadata object (spec.title, spec.date, spec.author, etc.) |
page |
Rendered HTML content |
date |
Build timestamp |
postSpecs |
Array of all post metadata (for navigation/sidebar) |
blogPosts |
Array of all posts (full objects) |
Example:
{% extends "layout.html" %}
{% block content %}
<article>
<h1>{{ spec.title }}</h1>
<time>{{ spec.date | date("F j, Y") }}</time>
{{ page | safe }}
</article>
{% endblock %}Renders index and archive pages. Additional variables:
| Variable | Description |
|---|---|
posts |
Array of posts for this page |
nextPage |
Next page number (or null) |
prevPage |
Previous page number (or null) |
Example:
{% for post in posts %}
<article>
<h2><a href="{{ post.spec.path }}">{{ post.spec.title }}</a></h2>
<time>{{ post.spec.date | date("F j, Y") }}</time>
</article>
{% endfor %}
{% if prevPage !== null %}
<a href="page{{ prevPage }}.html">Newer</a>
{% endif %}
{% if nextPage !== null %}
<a href="page{{ nextPage }}.html">Older</a>
{% endif %}Atom feed template. Same variables as index, but typically uses blogPosts for all posts.
A minimal config.json:
{
"templateRoot": "./templates/",
"outputRoot": "./output/",
"data": [
{
"id": "blogPosts",
"type": "markdown-posts",
"postRoot": "./posts/",
"path": "/{year}/{month}/{title}.html"
},
{
"id": "postSpecs",
"type": "post-specs",
"blogData": "blogPosts"
},
{
"id": "postTemplate",
"type": "nunjucks-template",
"file": "post.html"
},
{
"id": "indexTemplate",
"type": "nunjucks-template",
"file": "index.html"
},
{
"id": "feedTemplate",
"type": "nunjucks-template",
"file": "feed.xml"
}
],
"output": [
{
"id": "posts",
"type": "post-files",
"template": "postTemplate",
"data": ["date", "blogPosts", "postSpecs"]
},
{
"id": "index",
"type": "index-files",
"postsPerPage": 5,
"indexFile": "index.html",
"archiveFile": "page{number}.html",
"template": "indexTemplate",
"data": ["date", "blogPosts", "postSpecs"]
},
{
"id": "feed",
"type": "single-file",
"output": "atom.xml",
"template": "feedTemplate",
"data": ["date", "blogPosts"]
}
]
}Loaded sequentially (order matters - later sources can reference earlier ones).
| Type | Description |
|---|---|
markdown-posts |
Load posts from a directory via ssbl |
post-specs |
Extract just metadata from loaded posts |
nunjucks-template |
Compile a Nunjucks template |
multi-page-markdown |
Load markdown split into pages (for presentations) |
Run in parallel after all data is loaded.
| Type | Description |
|---|---|
single-file |
Render one template to one file |
post-files |
Render each post to its own file |
index-files |
Render paginated index/archive pages |
For build scripts or integration with other tools:
import Blorg, { archetypes } from 'blorg'
import { fileURLToPath } from 'node:url'
import { dirname } from 'node:path'
const __dirname = dirname(fileURLToPath(import.meta.url))
// Use the blog archetype for sensible defaults
const config = archetypes.blog({
outputRoot: './output/',
// Optional overrides:
// templateRoot: './templates/',
// postRoot: './posts/',
// postPath: '/{year}/{month}/{title}.html',
})
const blorg = new Blorg(__dirname, config)
await blorg.run()Posts are processed with Brucedown:
- GitHub Flavoured Markdown (tables, strikethrough, task lists)
- Shiki syntax highlighting (VS Code quality, 200+ languages)
- Output uses inline styles (no CSS required for code blocks)
MIT Licence. Copyright (c) Rod Vagg.