Blog Home
Updated: 2023 Oct 09

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

  1. A lightweight website with mobile scaling
  2. Easily rebuild my website and sync it to any web server
  3. Do so without adding any new software dependencies (just Emacs and ssh)

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

We need to tell Org Publish about our project layout by defining the org-publish-project-alist

(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 filesfff as specified by the :base-extension and exports the files to html as per the :publishing-function. The "static" component handles other files like images and style sheets.

For each project component the following options should 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 HTML
    • org-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 with:

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

Additional Options

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=\"/\">&lt; Home</a>
</nav>
<div id=\"updated\">Updated: %C</div>"

:html-postamble "<hr/>
<footer>
  <div class=\"copyright-container\">
    <div class=\"copyright\">
      Copyright &copy; 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 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:

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 do not intent to distribute the source code of your website or move the directory around frequently. The simplest way to configure Org publish is to place settings directly in your Emacs init with hard coded directory paths.

However, I've released the source of my website and therefore hard coded paths will break when project folder moves around. For that reason I keep all of my Org publish configuration in publish.el. From publish.el we can get the complete project path:

(defun my/relative-path-expand (path)
  "Expand relative PATH from current buffer or file to a full path."
  (concat
   (if load-file-name
       (file-name-directory load-file-name)
     default-directory)
   path))

Now we can dynamically set our base directory to the full path:

:base-directory  ,(my/relative-path-expand "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.

$ go get github.com/golang/example/hello
$ go get github.com/golang/example/hello
$ go get github.com/golang/example/hello

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 solution for determining the path of an Emacs Lisp file.

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.

See Also

Comments:

Email questions, comments, and corrections to hi@smartisan.dev.

Submissions may appear publicly on this website, unless requested otherwise in your email.