An Emacs static site generator. Write your content in org-mode, configure everything in your Emacs config, build with a single command.
Just Emacs, org-mode, and HTML templates!
M-x package-install RET org-grimoire RETOr with use package:
(use-package org-grimoire
:ensure t)Clone the repository and add it to your load path:
(add-to-list 'load-path "~/path/to/org-grimoire/")
(require 'org-grimoire)simple-httpd is not required to build a site, but if it is installed
M-x org-grimoire-serve will serve your built site at
http://localhost:8080 for a quick local preview.
Add to your Emacs config:
(org-grimoire-setup "my-blog"
:base-dir "~/blog"
:base-url "https://yoursite.com"
:site-title "My Blog"
:description "A blog about things"
:theme "mytheme"
:per-page 10)org-grimoire derives all paths from :base-dir automatically:
| Key | Derived path |
|---|---|
:source | base-dir/content/ |
:output | base-dir/public_html/ |
:static | base-dir/static/ |
:theme | base-dir/themes/mytheme/ |
You can override any of these individually by passing :source,
:output, :static, or a full path to :theme explicitly.
All available setup keys:
| Key | Required | Description |
|---|---|---|
:base-dir | yes | Root directory of your site |
:base-url | yes | Full URL including scheme (e.g. https://example.com) |
:site-title | yes | Global site name, used in feeds and navigation |
:description | no | Site description, used in feeds and meta tags |
:author | no | Author name, used in Atom feed |
:theme | no | Theme name under base-dir/themes/ |
:per-page | no | Posts per index page, defaults to 10 |
:reading-time | no | Set to t to enable reading time estimates |
:index-exclude-tags | no | Tag name(s) to keep off the index; string or list |
Posts carrying any tag listed in :index-exclude-tags are left off the
index and its pagination. They are still rendered, and still appear on
tag pages, in the feeds and in the sitemap. This is useful when one
category dominates your output and you would rather browse it at
/tags/name.html:
(org-grimoire-setup "my-blog"
:base-dir "~/blog"
:base-url "https://yoursite.com"
:site-title "My Blog"
:index-exclude-tags '("ctf"))Matching is case-insensitive, and a bare string is accepted for a
single tag: :index-exclude-tags "ctf".
For per-post control, use #+LISTED: nil instead.
You can configure and build multiple sites independently:
(org-grimoire-setup "blog"
:base-dir "~/blog"
:base-url "https://blog.com"
:site-title "My Blog"
:theme "minimal")
(org-grimoire-setup "company"
:base-dir "~/company-site"
:base-url "https://company.com"
:site-title "My Company"
:theme "corporate")
(org-grimoire-build "blog")
(org-grimoire-build "company")blog/ ├── content/ # your org files │ ├── post/ # content type inferred from directory name │ │ ├── my-post.org │ │ └── images/ │ │ └── screenshot.png │ └── page/ │ └── about.org ├── themes/ │ └── mytheme/ # HTML templates │ ├── base.html │ ├── post.html │ ├── page.html │ ├── index.html │ ├── tags.html │ ├── static/ # theme static files (css, fonts, etc.) │ └── partials/ │ ├── navbar.html │ ├── footer.html │ ├── post-item.html │ ├── tag-item.html │ ├── tag-index.html │ └── pagination.html ├── static/ # your static files, copied as-is to output/static/ │ └── css/ │ └── style.css └── public_html/ # generated output
The content type is inferred from the directory the file lives in.
A file in content/post/ is of type post and uses post.html template.
A file in content/page/ is of type page and uses page.html, and so on.
You can create any number of content types by adding subdirectories.
#+TITLE: My Post
#+DATE: 2024-01-15
#+TAGS: emacs lisp
Post content goes here. Full org-mode syntax supported.
| Keyword | Description | Example |
|---|---|---|
#+TITLE | Page title | #+TITLE: My Post |
#+DATE | Publication date (yyyy-mm-dd) | #+DATE: 2024-01-15 |
#+TAGS | Space or comma separated list of tags | #+TAGS: emacs lisp |
#+DRAFT | Set to t to exclude from build | #+DRAFT: t |
#+LISTED | Set to nil to hide from index | #+LISTED: nil |
#+DRAFT: t— post is not built at all, completely excluded#+LISTED: nil— post is built and accessible via its URL and tag pages, but does not appear on the main index listing. Defaults totif omitted.
org-grimoire automatically detects file: links in your org files
and copies referenced files to the output directory, preserving
relative paths. This means org-download works out of the box:
[[file:images/screenshot.png]]
Gets copied from content/post/images/screenshot.png to
public_html/post/images/screenshot.png and the relative link works
in the browser with no extra configuration.
:theme is expanded against your :base-dir, so :theme "mytheme"
means base-dir/themes/mytheme/. Pass a full path to point somewhere
else.
Template lookup falls back per file: each template is looked for in your
theme first, and anything missing is taken from the built-in default
theme that ships with the package. A theme can therefore override just
post.html and inherit everything else.
Static files do not merge. Your theme’s static/ directory is copied
as-is, so a theme that ships one is responsible for all of it. The
default theme’s static files are used only when the theme directory
itself is missing, which is what makes a site with no theme of its own
come out styled.
M-x org-grimoire-init writes an editable copy of the default theme to
base-dir/themes/default/ to start from.
base.html is the site-wide layout. All pages are wrapped in it.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{title}}</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
{{include partials/navbar.html}}
{{content}}
{{include partials/footer.html}}
</body>
</html>Available placeholders in base.html:
| Placeholder | Description |
|---|---|
{{title}} | Current page title |
{{site-title}} | Global site title from :site-title |
{{description}}=| Site description from =:description | |
{{author}} | Author from :author |
{{base-url}} | Base URL from :base-url |
{{url}} | Root-relative URL of the current page |
{{content}} | Rendered page content |
Each content type maps to its own template file. A file in content/post/
uses post.html, a file in content/page/ uses page.html, and so on.
post.html:
<article>
<h1>{{title}}</h1>
<time>{{date}}</time>
{{tags}}
<span>{{reading-time}}</span>
<div class="content">{{content}}</div>
</article>Available placeholders in type templates:
| Placeholder | Description |
|---|---|
{{title}} | Post title |
{{content}} | Rendered HTML content |
{{date}} | Publication date |
{{tags}} | Rendered tag links (grimoire-tags div) |
{{slug}} | URL slug derived from filename |
{{reading-time}} | Estimated reading time (e.g. “3 min read”), requires :reading-time t in setup |
index.html is used for the paginated post listing at the site root.
<h1>{{site-title}}</h1>
<p>{{description}}</p>
<ul>{{posts}}</ul>
{{pagination}}| Placeholder | Description |
|---|---|
{{site-title}} | Global site title |
| ={{description}}= | Site description |
{{posts}} | Generated list of post items |
{{pagination}} | Previous/next page navigation |
tags.html is the tags index at /tags/index.html, listing all tags:
<h1>{{title}}</h1>
<ul>{{tags}}</ul>partials/tag-index.html is used for individual tag listing pages at
/tags/tag-name.html:
<h1>{{title}}</h1>
<ul>{{posts}}</ul>partials/tag-item.html renders a single tag in the tags index:
| Placeholder | Description |
|---|---|
{{name}} | Tag name |
{{slug}} | URL-safe tag slug |
{{count}} | Number of posts with tag |
Any template can include another file using {{include}}:
{{include partials/navbar.html}}
{{include partials/footer.html}}Files are resolved relative to your theme directory, with fallback to the built-in default theme.
| Command | Description |
|---|---|
org-grimoire-init | Initialize a new site with boilerplate structure |
org-grimoire-setup | Register a site configuration |
org-grimoire-build | Build the site |
org-grimoire-new | Create a new post interactively |
org-grimoire-serve | Serve the site locally (requires simple-httpd) |
Run M-x org-grimoire-build and enter the site name when prompted, or
call it directly:
(org-grimoire-build "my-blog")Output goes to base-dir/public_html/. Build log is written to the
*Messages* buffer. Errors in individual files are logged as warnings
and the build continues, with a summary printed at the end.
Run M-x org-grimoire-init to create a new site with a boilerplate
directory structure, sample post and page, and a config snippet to add
to your Emacs config.
Run M-x org-grimoire-new and enter the site name. You will be prompted
to pick a content type from existing directories, then enter a title and
tags. The file is created and opened automatically as a draft.
If you have simple-httpd installed, run M-x org-grimoire-serve to
serve your built site at http://localhost:8080. simple-httpd is not
a required dependency.
RSS and Atom feeds are automatically generated at /rss.xml and
/atom.xml from all published posts.
A sitemap is generated at /sitemap.xml.
org-grimoire is free software, released under the GNU General Public
License version 3 or later. See the LICENSE file for details.
