Folders and files
| Name | Name | 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.