Building a Emacs Org-Mode Blog
As my WordPress website nears the end of its subscription, I've decided this would be a perfect opportunity to build my own website using a pure Emacs and Org mode setup. While using WordPress I already composed my posts in Org mode and published them using the org2blog package. This works fine, but WordPress is overkill as I don't the editor, themes, or plugins. A simpler solution would be to utilize the HTML exporter built into Org mode. The result is a simple, fast website, built entirely with Emacs.
Goals
- A lightweight website with mobile scaling.
- Easily rebuild my website and sync it to any web server.
- Without adding any new software dependencies.
Tools
Editor | GNU Emacs |
Markup Language | Org mode |
Site Generator | Org mode ox-publish |
Syncing | tramp and ssh |
Initial Setup
Out of the box Org mode provides many of different exporters for HTML, LaTeX, even plain text. In addition to standard HTML exporting Org provides ox-publish specifically for publishing Org projects as websites.
For my website, I use the following project structure:
~/taingram.org/ ├── org/ # content to be exported │ ├── blog/ │ │ └── example-post.org │ ├── img/ │ ├── index.org │ ├── robots.txt │ └── style.css ├── html/ # temporary export directory ├── publish.el # site sepecifc publishing config └── Makefile # for building the site outside of Emacs
To explain this structure to Org publish we need to set the
org-publish-project-alist
as follows
(require 'ox-publish) (setq org-publish-project-alist `(("pages" :base-directory "~/taingram.org/org/" :base-extension "org" :recursive t :publishing-directory "~/taingram.or/html/" :publishing-function org-html-publish-to-html) ("static" :base-directory "~/taingram.org/org/" :base-extension "css\\|txt\\|jpg\\|gif\\|png" :recursive t :publishing-directory "~/taingram.org/html/" :publishing-function org-publish-attachment) ("taingram.org" :components ("pages" "static"))))
From the last line we can see "taingram.org" is broken down into the
components "pages" and "static". Each component handles a different part of
the website and has its own settings. The "pages" component handles org files
as specified by the :base-extension
and exports the files to html as per the
:publishing-function
. The "static" component handles copying over other
files like images and style sheets.
For each project component the following options should always be set:
:base-directory
— where the source files are:base-extension
— what type of file (e.g. org) is being exported:publishing-directory
— where the exported files are going:publishing-function
— how the files are exported. Some options are:org-html-publish-to-html
— converts org files to HTMLorg-publish-attachment
— copies the files verbatim
Exporting
Evaluating above code is enough to enable publishing. Open an org file within
your project and type C-c C-e to bring up the "Org Export
Dispatcher", type P for Publishing, and a to publish all. The site will
be generated and placed in the html/
directory.
You can also publish the site from elisp by evaluating:
(org-publish "taingram.org" t)
The t
argument is optional and will force every file to be re-export, even
if that file has not changed. Useful if you are experimenting with project
settings without changing org files.
Testing your Website
To test out your website with working links you will need a web
server. Python includes one we can use, from the html/
directory run
python3 -m http.server
Open http://localhost:8000 in a browser to see your new website.
Further Customization
Now that we have a basic website exporting we can start experimenting with more advanced settings within Org publish.
Customize the <Head>
We can tweak the way HTML is exported with a number of :html-*
options. I
prefer to use HTML5 and disable Org's default style sheet and scripts:
; HTML5 :html-doctype "html5" :html-html5-fancy t ; Disable some Org's HTML defaults :html-head-include-scripts nil :html-head-include-default-style nil
CSS
Org's HTML exporter makes it easy to create your own style sheet. The exported HTML makes sensible use of standard tags as well as specifying number of classes documented under CSS Support in the Org manual. For my site I wrote my own style sheet and linked it with:
:html-head "<link rel=\"stylesheet\" href=\"/style.css\" type=\"text/css\"/>"
The :html-head
option lets you add any arbitrary HTML to the head tag.
Preamble and Postamble
Like :html-head
Org Publish options for inserting extra HTML before and
after your post content using :html-preamble
and :html-postamble
.
You can add as much (or little) additional HTML as you would like. I used the preamble to insert some navigation and when the page was updated. The postamble has a footer with copyright information and site creation info.
:html-preamble "<nav> <a href=\"/\">< Home</a> </nav> <div id=\"updated\">Updated: %C</div>" :html-postamble "<hr/> <footer> <div class=\"copyright-container\"> <div class=\"copyright\"> Copyright © 2017-2020 Thomas Ingram some rights reserved<br/> Content is available under <a rel=\"license\" href=\"http://creativecommons.org/licenses/by-sa/4.0/\"> CC-BY-SA 4.0 </a> unless otherwise noted </div> <div class=\"cc-badge\"> <a rel=\"license\" href=\"http://creativecommons.org/licenses/by-sa/4.0/\"> <img alt=\"Creative Commons License\" src=\"https://i.creativecommons.org/l/by-sa/4.0/88x31.png\" /> </a> </div> </div> <div class=\"generated\"> Created with %c on <a href=\"https://www.gnu.org\">GNU</a>/<a href=\"https://www.kernel.org/\">Linux</a> </div> </footer>"
Note the use of '%c' and '%C', these symbols will be expanded by Org's html
exporter. Their meaning is documented in the org-html-preamble-format
variable. Here is the complete list:
%t | stands for the title. |
%s | stands for the subtitle. |
%a | stands for the author’s name. |
%e | stands for the author’s email. |
%d | stands for the date. |
%c | will be replaced by ‘org-html-creator-string’. |
%v | will be replaced by ‘org-html-validation-link’. |
%T | will be replaced by the export time. |
%C | will be replaced by the last modification time. |
Sitemap
Org publish can generate a sitemap for projects, essentially a site wide table of contents with links and directory structure. For a global sitemap in your website add the following to the "pages" project component:
:auto-sitemap t :sitemap-filename "sitemap.org"
For example, a global sitemap for taingram.org would appear as follows:
Once the sitemap.org is generated you can include it from any other page with
#+INCLUDE: sitemap.org :lines "3-"
The :lines 3-
will only include the 3rd line on, skipping the #+TITLE
tag
set in sitemap.org and grabbing only the list of pages as shown above.
Creating a List of Blog Posts
While a global sitemap can be useful, I want a greater distinction made
between blog posts and regular pages. This can be accomplish by separating
"pages" into two components: "pages" in the base directory and "blog" posts
under org/blog/
("pages" :base-directory "~/taingram.org/org/" :base-extension "org" :recursive nil ; avoid exporting blog twice :publishing-directory "~/taingram.org/html/" :publishing-function org-html-publish-to-html) ("blog" :base-directory "~/taingram.org/org/org/blog/" :base-extension "org" :publishing-directory "~/taingram.org/org/html/blog/" :publishing-function org-html-publish-to-html :auto-sitemap t :sitemap-title "Blog Posts" :sitemap-filename "index.org" :sitemap-sort-files anti-chronologically) ("taingram.org" :components ("pages" "blog" "static"))))
With these settings "blog" will have a sitemap in blog/index.org
that
contains a list of only blog posts. The option :sitemap-sort-files
anti-chronologically
will sort the posts from newest to oldest.
Now say you have written a homepage in index.org
and would like to have
your list of recent blog posts, again include with
* Blog Posts #+INCLUDE: blog/blog.org :lines "3-8" [[file:blog/index.org][See more...]]
Custom sitemap entries
To take this one step further, we can customize the entry format with a
sitemap-format-entry
function. In my case I wanted to show the date inline
with the blog post listings:
(defun my/org-sitemap-date-entry-format (entry style project) "Format ENTRY in org-publish PROJECT Sitemap format ENTRY ENTRY STYLE format that includes date." (let ((filename (org-publish-find-title entry project))) (if (= (length filename) 0) (format "*%s*" entry) (format "{{{timestamp(%s)}}} [[file:%s][%s]]" (format-time-string "%Y-%m-%d" (org-publish-find-date entry project)) entry filename))))
Notice (format "{{{timestamp(%s)}}} [[file:%s][%s]]")
inserts an Org macro
called timestamp, it is defined as follows:
(setq org-export-global-macros '(("timestamp" . "@@html:<span class=\"timestamp\">[$1]</span>@@")))
This macro adds some HTML around the timestamp for CSS styling, it has to be done as a macro as otherwise Org escapes the HTML tags. The results are:
- Building a Blog with Emacs Org-Mode
- Emacsclient Setup with Desktop Integration
- Fixing Dell XPS 13 Audio Popping
Further styling is added on the homepage by wrapping the list in an additional div class:
#+HTML: <div class="blog-entries"> #+INCLUDE: "blog/index.org" :lines "3-" #+HTML: </div>
Building and Publishing
Now that we have our website looking more professional we need to publish it
to the web server. A fast and simple way is to copy the html/
directory with
rsync:
rsync -e ssh -uvr html/ thomas@taingram.org:/var/www/taingram.org/html/
Publish Over Tramp
Another option is to publish directly to your web server using TRAMP. TRAMP
(Transparent Remote (file) Access, Multiple Protocol) is a tool built into
Emacs for accessing files on remote servers. The format for accessing a file
over TRAMP is /method:user@host:/path/to/file
and can be used directly in
Emacs find file dialog.
We can simply replace our :publishing-directory
with the tramp format:
:publishing-directory "/ssh:thomas@taingram.org:/var/www/taingram.org/html/"
Just like that when we publish our file they will be sent directly to our server. Convenient for publishing individual files, but will be much slower than the rsync solution.
Relative Directory Paths
If you intend to distribute the source code of your website (or
otherwise move the project directory) then you'll run into the
issue of breaking paths. To get around this I keep my publishing
config in a file publish.el
with the rest of my website's source
code. From publish.el
we can determine the complete project path
using:
(defvar taingram--project-directory (file-name-directory (or load-file-name (buffer-file-name))))
Now we can dynamically set our base directory to the full path:
:base-directory ,(concat taingram--project-directory "org/")
Note for this to work your org-publish-project-alist
should be started
with a ` (backquote) which enables code after a comma to be evaluated. See
backquote in the Emacs Lisp Manual.
Thanks
I have always found the Emacs community to be full of extremely knowledgeable and helpful individuals. I would like to thank Thibault Marin on the emacs-orgmode mailing list for his help fixing my custom sitemap function with the suggestion of using an Org mode macro.
Thanks to Lindydancer on Stack Overflow for the initial solution for determining the path of an Emacs Lisp file, and thanks to u/nv-elisp on reddit for their improved solution.
Finally, thank you to all developers of Org mode for producing the best text based organization system in existence. Specifically thanks to David O’Toole who originally contributed Org Publish.
Comments
Email comments and corrections to comment@taingram.org.
Submissions may be posted publicly unless requested otherwise in your email.