Skip to content

Latest commit

 

History

14 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

#+OPTIONS: toc:nil
* org-grimoire — An Emacs-native Static Site Generator
[[https://github.com/spiperac/org-grimoire/actions/workflows/melpazoid.yml][https://github.com/spiperac/org-grimoire/actions/workflows/melpazoid.yml/badge.svg]]
[[https://stable.melpa.org/#/org-grimoire][file:https://stable.melpa.org/packages/org-grimoire-badge.svg]]
[[https://melpa.org/#/org-grimoire][file:https://melpa.org/packages/org-grimoire-badge.svg]]

#+ATTR_HTML: :width 128px
[[file:docs/logo.png]]

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!

** Installation

*** MELPA

#+BEGIN_SRC elisp
M-x package-install RET org-grimoire RET
#+END_SRC

Or with use package:

#+BEGIN_SRC elisp
(use-package org-grimoire
  :ensure t)
#+END_SRC

*** Manual

Clone the repository and add it to your load path:

#+begin_src elisp
(add-to-list 'load-path "~/path/to/org-grimoire/")
(require 'org-grimoire)
#+end_src

*** Optional dependencies

=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.

** Configuration

Add to your Emacs config:

#+begin_src elisp
(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)
#+end_src

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    |

*** Excluding tags from the index

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=:

#+begin_src elisp
(org-grimoire-setup "my-blog"
  :base-dir           "~/blog"
  :base-url           "https://yoursite.com"
  :site-title         "My Blog"
  :index-exclude-tags '("ctf"))
#+end_src

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.

*** Multiple sites

You can configure and build multiple sites independently:

#+begin_src elisp
(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")
#+end_src

** Directory structure

#+begin_src
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
#+end_src

** Writing content

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.

#+begin_src org
,#+TITLE: My Post
,#+DATE: 2024-01-15
,#+TAGS: emacs lisp

Post content goes here. Full org-mode syntax supported.
#+end_src

*** Frontmatter keywords

| 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 vs Listed

- =#+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 to
  =t= if omitted.

*** Images and file links

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:

#+begin_src org
[[file:images/screenshot.png]]
#+end_src

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.

** Templates

*** Theme resolution

=: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 template

=base.html= is the site-wide layout. All pages are wrapped in it.

#+begin_src html
<!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>
#+end_src

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                    |

*** Type templates

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=:
#+begin_src html
<article>
  <h1>{{title}}</h1>
  <time>{{date}}</time>
  {{tags}}
  <span>{{reading-time}}</span>
  <div class="content">{{content}}</div>
</article>
#+end_src

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 template

=index.html= is used for the paginated post listing at the site root.

#+begin_src html
<h1>{{site-title}}</h1>
<p>{{description}}</p>
<ul>{{posts}}</ul>
{{pagination}}
#+end_src

| Placeholder      | Description                   |
|------------------+-------------------------------|
| ={{site-title}}= | Global site title             |
| ={{description}}=| Site description              |
| ={{posts}}=      | Generated list of post items  |
| ={{pagination}}= | Previous/next page navigation |

*** Tags templates

=tags.html= is the tags index at =/tags/index.html=, listing all tags:

#+begin_src html
<h1>{{title}}</h1>
<ul>{{tags}}</ul>
#+end_src

=partials/tag-index.html= is used for individual tag listing pages at
=/tags/tag-name.html=:

#+begin_src html
<h1>{{title}}</h1>
<ul>{{posts}}</ul>
#+end_src

=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 |

*** Includes

Any template can include another file using ={{include}}=:

#+begin_src html
{{include partials/navbar.html}}
{{include partials/footer.html}}
#+end_src

Files are resolved relative to your theme directory, with fallback to
the built-in default theme.

** Commands

| 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=) |

*** Building

Run =M-x org-grimoire-build= and enter the site name when prompted, or
call it directly:

#+begin_src elisp
(org-grimoire-build "my-blog")
#+end_src

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.

*** Starting a new site

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.

*** Creating new content

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.

*** Local preview

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.

** Feeds

RSS and Atom feeds are automatically generated at =/rss.xml= and
=/atom.xml= from all published posts.

A sitemap is generated at =/sitemap.xml=.

** License

org-grimoire is free software, released under the GNU General Public
License version 3 or later. See the =LICENSE= file for details.

About

Static Site Generator writen in Elisp, running from GNU/Emacs.

Topics

Resources

Stars

30 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages