Blog Home
Updated: 2024-06-21

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.


  1. A lightweight website with mobile scaling.
  2. Easily rebuild my website and sync it to any web server.
  3. Without adding any new software dependencies.


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:

├── org/            # content to be exported
│   ├── blog/
│   │   └──
│   ├── img/
│   ├──
│   ├── 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
         :base-directory "~/"
         :base-extension "org"
         :recursive t
         :publishing-directory "~/taingram.or/html/"
         :publishing-function org-html-publish-to-html)

         :base-directory "~/"
         :base-extension "css\\|txt\\|jpg\\|gif\\|png"
         :recursive t
         :publishing-directory  "~/"
         :publishing-function org-publish-attachment)

        ("" :components ("pages" "static"))))

From the last line we can see "" 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 HTML
    • org-publish-attachment — copies the files verbatim


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

:html-doctype "html5"
:html-html5-fancy t

; Disable some Org's HTML defaults
:html-head-include-scripts nil
:html-head-include-default-style nil


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>
<div id=\"updated\">Updated: %C</div>"

:html-postamble "<hr/>
  <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=\"\">
        CC-BY-SA 4.0
      </a> unless otherwise noted
    <div class=\"cc-badge\">
      <a rel=\"license\" href=\"\">
        <img alt=\"Creative Commons License\"
             src=\"\" />

  <div class=\"generated\">
    Created with %c on <a href=\"\">GNU</a>/<a href=\"\">Linux</a>

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.


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

For example, a global sitemap for would appear as follows:

Once the is generated you can include it from any other page with

#+INCLUDE: :lines "3-"

The :lines 3- will only include the 3rd line on, skipping the #+TITLE tag set in 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/

 :base-directory "~/"
 :base-extension "org"
 :recursive nil                               ; avoid exporting blog twice
 :publishing-directory "~/"
 :publishing-function org-html-publish-to-html)
 :base-directory "~/"
 :base-extension "org"
 :publishing-directory "~/"
 :publishing-function org-html-publish-to-html

 :auto-sitemap t
 :sitemap-title "Blog Posts"
 :sitemap-filename ""
 :sitemap-sort-files anti-chronologically)
("" :components ("pages" "blog" "static"))))

With these settings "blog" will have a sitemap in blog/ 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 and would like to have your list of recent blog posts, again include with

* Blog Posts
#+INCLUDE: blog/ :lines "3-8"
[[file:blog/][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))

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/" :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/

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 "/"

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.


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.

See Also


Email comments and corrections to

Submissions may be posted publicly unless requested otherwise in your email.