Christian Moe
writer and translator
| | |

A mostly static website with Emacs Orgmode

Very much a work in progress.

These notes document why and how I’ve used Emacs Org-mode to make this site, with some tips on how to do so yourself for a moderately complex personal website with some added bells and whistles.

Introduction: dynamic vs. static sites

When I first learned to make web pages, we just hand-coded them in HTML and saved them to folders as text files. There they would stay put until someone pointed their browser e.g. to http://christianmoe.com/en/notes/static-site.html, which let the web server know to retrieve the page static-site.html from the en/notes folder.

Today, even the humblest of blogs is typically a dynamic site driven by a Content Management System (CMS) with a lot of moving parts: not just a web server, but a database and a web application with thousands of lines of PHP slaving away behind the scenes to put together pages on the fly. The folders are virtual and the whole content is just one database file.

The best CMSes offer many advantages:

  • Content is easily searched, sorted, filtered, listed, combined…
  • Virtual folder structures are easily changed, permalinks remain.
  • Common elements, like a list of latest blog posts, are easily added across all pages.
  • Changes can be automatically logged.
  • Migration to/from a different platform is sometimes supported.
  • Authenticated users can be assigned roles in a workflow to create content together; certain contect can be restricted to certain users.
  • Comments, pingbacks, and even Fediverse integration can be handled automatically.
  • There is support, community, and a well-honed codebase from which to learn if you want to tinker for yourself (assuming it’s open source).
  • Reasonable security precautions have hopefully been taken by people who know what they’re doing.

But if you just want your own personal website, without breaking news and archives of thousands of pages, you don’t really need all of this. So some people turn to static-site generators instead. These take care of a lot of the same housekeeping for you as CMSes do (adding common page elements, tags, timestamps etc.).

But instead of putting documents together on the fly every time someone wants to read them, static-site generators only put them together for you every time you press “publish.” They also typically allow you to write in a plain-text format, usually some flavor of Markdown, and convert it to HTML, which saves typing a lot of angle brackets.

Static sites are simple, fast, and secure. The server only has to find the page and serve it, not execute a lot of code and database queries.

Also, CMSes tend to expect you to type your thoughts into a browser window. There are better ways to write.

Emacs and Org-mode

There are a few things Org-mode doesn’t easily do, and that I sometimes miss. For one, it’s great for writing long structured documents, but lacks the option of “chunking” them into multiple output files (as of summer 2024, though, there is serious talk on the Org mailing list about providing this functionality). Meanwhile, some people have written code to blog that way with various publishing solutions (org-to-blog; this for Hugo, and this for Jekyll, discussed here).

For me, a better way to write is with the Org-mode major mode of the Emacs text editor. If Emacs is practically an operating system, then Org-mode is practically an office package in plain text: calendar, tasks, lists, notes, tags, links, simple database features, totally awesome plain-text tables that double as a powerful spreadsheet, and in recent years, top-notch native support for citations and bibliographies. Unlike your typical office package, it can also mix your text with code. All this, including both the code and its output, can be exported to various formats. And it can be published to the web.

If I used a CMS, I would use Org-mode to draft content anyway, export it to HTML and paste it in, as I used to do in my Wordpress blog on climate. So why not just publish it directly from Org-mode?

Some people use Org-mode in combination with static-site generators such as Jekyll, sometimes with tricks to get the best of both worlds. For this site, I have tried exploring what can be done using Org-mode’s publishing feature out of the box, plus a few added scripts that a hobbyist like me could write on their own.

Publishing with Org-mode

To publish a website with Org, you write the content as Org files in the same folder structure you want on your website. The source of this page is the file ~/www/cm/en/staticsite.org, which as you see lies in the en subfolder of the cm project in my www folder.

Next, you have to set up one or more publishing projects. You can read all about it in the manual. (If you’re not familiar with Emacs, you should be aware that you can configure it any way you like by writing code in the Emacs-Lisp programming language in a configuration file that typically lives in your home directory and is called .emacs.) Basically, you define the publishing project by listing your requirements in a Lisp expression that you assign to the variable org-publish-project-alist in your Emacs configuration file. It tells Org-mode where to find your project, where to publish it, and what to do with the pages that are part of it.

Here is the general structure of my configuration right now (Listing 1):

(setq org-publish-project-alist
        '(("Landing"
           :base-directory "~/www/cm/"
           ; (...)
           )
          ("Norsk"
           :base-directory "~/www/cm/nb"
           ; (...)
           )
          ("Esperanto"
           :base-directory "~/www/cm/eo"
           ; (...)
           )
          ("English"
           :base-directory "~/www/cm/en"
           ; (...)
           )
          ("Slovensko"
           :base-directory "~/www/cm/sl"
           ; (...)
           )
          ("CSS" ; and .js scripts
           :base-directory "~/www/cm/css"
           :base-extension "css\\|js"
           ; (...)
           )
          ("media" ; images, HTML, PDF, XML (RSS), raw text, webfonts
           :base-directory "~/www/cm/"
           :base-extension "jpg\\|gif\\|png\\|ico\\|svg\\|htm\\|html\\|pdf\\|xml\\|\\txt\\|ttf\\|woff2"
           ; (...)
           :recursive t)
          ("RSS"
           :base-directory "~/www/cm/"
         :preparation-function cm/write-newsfeed
         ; (...)
          ("ChristianMoe.com"
           :components ("Landing" "English" "Norsk" "Esperanto" "Slovensko" "CSS" "media" "RSS"))))

As you see at the bottom of the list, a project (ChristianMoe.com) can have several component subprojects (Landing, Norsk, English etc.). Importantly, for each subproject you can configure a bunch of export options telling Org how to format your exported document. This allows you to set up different rules for what to do with text files and images, or with files in different subfolders. Here is the full content of the Norwegian (“Norsk”) subproject:

("Norsk"
 :base-directory "~/www/cm/nb"
 :publishing-directory "/var/www/html/nb"
 :language "nb"
 :publishing-function org-html-publish-to-html
 :html-doctype "xhtml-strict"
 :recursive t
 :exclude "^\\."
 :section-numbers nil
 :with-toc nil
 :html-preamble cm/html-preamble:nb
 :html-postamble t
 :auto-sitemap t
 ;; :sitemap-function org-publish-sitemap
 :sitemap-sort-folders last
 :sitemap-title "Nettstedskart"
 :sitemap-sans-extension t
 :sitemap-style tree
 :sitemap-file-entry-format "%t" ;"@@html:<b>%t</b><br><i>Lagt ut %d av %a</i><br>" ; Keywords: %k<br>%e@@"
 :makeindex t
 :html-head-include-default-style nil
 :html-head-include-scripts nil
 :html-head "<link rel=\"stylesheet\" href=\"/css/cm.css\" type=\"text/css\"/><script src=\"/css/jquery-1.9.1.js\" type=\"text/javascript\"></script><script src=\"/css/tagindex.js\" type=\"text/javascript\"></script><script src=\"/css/cm.js\" type=\"text/javascript\"></script><link rel=\"icon\" href=\"/img/favicon.png\" />")

You can still also add export options on a per-page basis. For most of these pages I don’t want auto-generated tables of contents, hence I have :with-toc nil in the config, but in this page I have ordered one by adding #+OPTIONS: toc:t to the top of static-site.org.

You can also tell Org-mode what to put in the html HEAD of each document. In the three lines at the end that start with :html-head-, I tell it not to use the default CSS and scripts Org HTML export offers (which are good), but to use mine instead. Again, you can very easily extend or override this on a per-page basis by putting a stylesheet link or a <style> section after #+HTML_HEAD: or #+HTML_HEAD_EXTRA: on top of the page.

You can also configure a preamble to be inserted before the content of each page, and a postamble afterwards. This is how I add the site banner and menu (preamble) and the right or bottom colophon (postamble) to each page. The simple but powerful out-of-the-box way to do this is by defining the variables org-publish-preamble-format and org-html-postamble-format. These are associative lists of HTML strings keyed to language, so you can have different postambles on a per-language basis, and they comes with a number of format strings (%t for title, %d for date, etc.) for templating, so for many purposes, they give you all the flexibility you need for a multilingual site. I do this for the postamble by setting :html-postamble t. As you see, however, to generate the :html-preamble I’m using a homemade function instead, because I want to do things a little differently not only per language but also e.g. depending on whether the page is a blog entry or not, and because it also lets me call other functions for housekeeping.

Styles, scripts, images and attachments

By default, Org publishing will overlook files not ending in .org. I have used a “CSS” subproject to also publish stylesheets, scripts, and webfonts, recognized by their file extensions, to a specific folder (and yes, I should rename it, since it’s not just CSS). Unlike .org files, obviously, these should not be exported to HTML with the function org-html-publish-to-html. Instead, I set the :publishing-function to org-publish-attachment, which simply transfers the files. Here’s the subproject of org-publish-project-alist:

("CSS" ; and .js scripts, and webfonts
 :base-directory "~/www/cm/css"
 :base-extension "css\\|js\\|ttf\\|woff2"
 :publishing-directory "/var/www/html/css"
 :publishing-function org-publish-attachment)

Media such as images are handled a little differently. Sometimes I’ll want to use a single image folder for cross-site purposes; often I’ll want to keep images in a folder with the page where they’re used. So I configure Org to recursively go through all the folders and to upload all the files it finds with an image extension (.jpg, .gif, .png, .svg) to a corresponding folder on the server, creating the folder if necessary. I do the same for PDFs, hand-crafted HTML docs, XML (for the RSS feed), and raw text documents. Here is the corresponding subproject of org-publish-project-alist:

("media" ; images, HTML, PDF,  XML (RSS), raw text
 :base-directory "~/www/cm/"
 :base-extension "jpg\\|gif\\|png\\|ico\\|svg\\|htm\\|html\\|pdf\\|xml\\|\\txt"
 :publishing-directory "/var/www/html"
 :publishing-function org-publish-attachment
 ;; Upload images and other media files to corresponding
 ;; folders throughout site regardless of sub-project
 :recursive t)

Make mine multilingual

One of my key requirements is a multilingual site. There is more than one way to do this, but using the modularity of org-publish seems like a sensible approach. By giving each language section its own base directory (folder), I can easily configure it as a subproject. Then I can let Org know to use Norwegian localization where available, insert a postamble in the appropriate language, etc., simply by setting this line in the subproject:

:language "nb"

That’s nb for Norwegian Bokmål. We’re so fond of the Norwegian language, we have two of it, the other being Nynorsk (nn). Typically no defaults to Bokmål, the majority language, but it seems a rude assumption to make.

I also have Org generate a sitemap for each language section.

I’m mostly using SIL’s Gentium Plus font, which has excellent Unicode support for Latin, Cyrillic, and Greek script.

Gentium Plus:

  • Norwegian: Ææ Øø Åå
  • Slovene and B/C/M/S with Latin script: Čč Ćć Đđ Šš Žž
  • Diacritics for Arabic transliteration: Āā Īī Ūū Ḍḍ Ḥḥ Ḳḳ Ṣṣ Ṭṭ Ẓẓ
  • Esperanto: Ĉĉ Ĝĝ Ĥĥ Ĵĵ Ŝŝ Ŭŭ
  • Other Cyrillic: Russian Аа Бб Вв Гг Дд Ее Ёё Жж Зз Ии Йй Кк Лл Мм Нн Оо Пп Рр Сс Тт Уу Фф Хх Цц Шш Щщ Ъъ Ыы Ьь Ээ Юю Яя; Belarusian Іі Ўў; Ukrainian Єє Її, Serbian Ђђ Јј Љљ Њњ Ћћ Џџ
  • Greek: Αα Ββ Γγ Δδ Εε Ζζ Ηη Θθ Ιι Κκ Λλ Μμ Νν Ξξ Οο Ππ Ρρ Σσς Ττ Υυ Φφ Χχ Ψψ Ωω

Gentium also looks nice without adjustments to the line height, which is a plus for inline math with MathJax.

I’d like to vary this a bit with a hand-writing font, but it’s hard to find a nice and readable one covering all of the above (see Notes: Hand-writing fonts for details).

Using filters

When exporting to HTML or other formats, Org lets you create custom “filters” to modify the output. I use one to add my name at the start of each <title> element, and add a <page> division just inside the HTML <body> for layout purposes. Another takes care that root-relative references work. True to my perl roots, these mostly consist of throwing regular expressions at the output text until it behaves.

  (defun cm/filter-site-pages (output backend info)
    "When publishing my site, add my name before the title and a
  .page div inside the body."
    (when (and (eq backend 'html)
                 (string-match "/www/cm/" (buffer-file-name)))
        (setq output (replace-regexp-in-string
                      "<title>\\(.*?\\)<br>\\(.*?\\)</title>"
                      "<title>\\1</title>" output)
              output (replace-regexp-in-string
                      "<title>" "<title>C. Moe | " output)
              output (replace-regexp-in-string
                      "<body>" "<body><div id=\"page\">" output)
              output (replace-regexp-in-string
                      "</body>" "</div><!-- page ends--></body>" output)))
    output)

(defun cm/filter-root-links (output backend info)
  "Fix any root-relative references in links."
  (when (and (eq backend 'html)
               (string-match "/www/cm/" (buffer-file-name)))
    (setq testvar output)
    (replace-regexp-in-string "file:///" "/" output)))

(add-to-list 'org-export-filter-final-output-functions
               'cm/filter-site-pages)
(add-to-list 'org-export-filter-link-functions
               'cm/filter-root-links)

Note that while the first function is added to org-export-filter-final-output-functions and so acts on the whole final html output, the second is added to org-export-filter-link-functions and so only acts on links, so I can write about a file:///path without getting it messed up.

Fancy content

Citations

Org-mode features org-cite: full-fledged native support for citations for any output format, with a basic citation processor out of the box, support for Citation Style Language (CSL) via András Simonyi’s citeproc-el processor for Emacs, as well as Bib(La)Tex processors for LaTeX exports. All of this simply works with export and publishing, but I include some notes here since I haven’t elsewhere.

Org-cite/citeproc reads bibliographic data in BibTeX (.bib) or CSL-JSON format, so you can use your hand-made BibTeX file if you like. I’m a long-time fan of the Zotero reference manager (also CSL-based), so I continually export a bibliography file from there with the Better BibTeX extension for Zotero to enable persistent keys.

For example, with the requisite setup pointing to my global bibliography file (exported from Zotero) and CSL styles directory (Zotero’s styles folder), I need only include [cite:@leha2011EmacsOrgmode] to cite a paper on using Org-mode for reproducible research (Leha and Beißbarth 2011). Specialized bibliographies and other styles can be specified with #+BIBLIOGRAPHY and #+CITE_EXPORT options. A bibliography can then be added with #+PRINT_BIBLIOGRAPHY: (the colon is required).

For language-appropriate citation formatting, you need to put the required CSL locale files for your languages in a folder, tell Org where to find them by defining org-cite-csl-locales-dir, and add indicate the document language with e.g. #+LANGUAGE: sl for Slovene. Esperanto took a little extra work: I had to make my own makeshift locale file, and because Esperanto is not associated with any country, I also had to add the "eo" language code to the list in citeproc-locale--simple-locales.

In fact, I originally drafted this site with my own homemade org-cite-zotero system, a bridge between Org-mode and Zotero, but I have converted my citations to the new officially supported syntax, since org-cite does everything as well or better (except embedding CoINs data).

Math

Org-mode is often simpler to work with and less cluttered than LaTeX, but luckily, you can mix LaTeX and Org very promiscuously. And Org works with the fabulous MathJax plugin for rendering LaTeX math on-the-fly in HTML (Listing 4):

I only have to type
\[ \iint x y^2 \mathrm{d}A 
 = \int_0^2 \left(\int_0^{x/2} xy^2 \mathrm{d}y
 \right) \mathrm{d}x \]

I only have to type

\[ \iint x y^2 \mathrm{d}A = \int_0^2 \left(\int_0^{x/2} xy^2 \mathrm{d}y \right) \mathrm{d}x \]

Double integral is also the shape of lovers curled asleep…

— Thomas Pynchon, Gravity’s Rainbow

and there you have it. How did I configure it, you ask? I didn’t. It just worked out of the box. (How do I solve it, you ask? I haven’t a clue, I just like the look.)

SVG images

SVG is a text format, so it should play well with a site that lives in plain text. At its simplest, we could simply include an SVG island in Org:

Here’s the code for that:

#+begin_export html
  <svg xmlns="http://www.w3.org/2000/svg" width="300" height="100">
     <circle cx="50" cy="50" r="50" fill="red" stroke="black" />
  </svg>
#+end_export

With the #+INCLUDE: statement, we can do the same with SVG that lives in a separate file. Here’s a Christmas tree I made.

image/svg+xml

To include it here, I type:

#+INCLUDE: "../../img/juletre.svg" :lines "2-" export html 

The include statement begins with the path to the SVG file. We need to omit the XML declaration, so I tell it to start on line 2. Finally, I tell it that the contents are to be wrapped in a #+BEGIN_EXPORT html block so that they are exported verbatim.

To style it, I can add a custom line of CSS to this page with just this line in the Org document header:

#+html_head_extra: <style>#xmastree { width: 150px; }</style>

Sometimes it’s better to link and embed an image:

#+attr_html: :width 80%
../../img/juletre.svg

which exports to

<div id="org69ff1e6" class="figure">
<p><img src="../../img/juletre.svg" alt="juletre.svg" class="org-svg" width="80%" />
</p>
</div>

and looks like

juletre.svg

Note that Org currently implements linked SVG image export with the <img> tag, which does not give you access to SVG internals the way <object> would. Here I have linked to the same file from which I made an <svg> island with #+include above, but this time the custom CSS does not work, because the xmastree id is an attribute of the <svg> element which the <img> approach does not expose.

Music

Org has support for Lilypond, so adding music can be as easy as writing a few lines of code in a #+begin_src lilypond environment:

\version "2.24.1"
 #(ly:set-option 'use-paper-size-for-page #f)
 #(ly:set-option 'tall-page-formats 'png)
\book {
  \paper {
        indent = 0\mm
        ragged-right = ##t
        oddFooterMarkup = ##f
  }
  \relative c' { a4 c d e | g a b c }
}

Keeping track of exported files and tags

Org-mode is great at tagging entries, but doesn’t leverage those tags for publishing searchable content. Org publishing offers a basic sitemap and the possibility of creating a book-style index. Neither is based on tags. It would be fairly easy to add code to the publishing process to link every tag to a dedicated page listing every other page with that tag. However, tags really come into their own when you can combine them, so I’ve looked for a way to do that. Which raises some questions and choices.

I either need to run a script on my server, or turn to Javascript to run one on your computer instead. I’ve gone for the latter. Javascript-based solutions will not work equally for all readers, but jQuery helps.

Tags can be assigned to headings, and with #+FILETAGS they can be assigned to a whole Org file, but the exporter does not automatically turn them into HTML keywords; for that, #+KEYWORDS is used. HTML keywords are comma-separated, so they allow spaces, which Org-mode tags don’t. Keywords and tags could in principle both be used as two different vocabularies. I’ve opted to treat them as one vocabulary, to use #+KEYWORDS in my .org files, and (at this point) to use only keywords for indexing, and to use heading tags only as clickable links, not targets. In the spirit of Org tags, I’ll use single-word keywords as much as I can, but I have left myself the future option of using underscores in tags and transforming them to spaces.

I have written an elisp function (cm/collect-exported-tags) to extract metadata from a file. To ensure that it’s run every time a page is published from this project, I invoke it from the function that puts the page preamble together. I extract the title, date, description, and keywords, and store them in two associative arrays. One, cm/exported-files, holds all the extracted information for each file, indexed by the unique file path. The other, cm/exported-tags, is indexed by tags, and holds lists of paths to all the files so tagged. (I could have kept all of this in one array, but somehow I find this arrangement easier to think about.) When a file is exported, I save the state of these variables as a lisp file, .tagindex, from which I load them back into memory the next time I export a file.

When publishing is done, this data structure is automatically converted to JSON and saved as a .js file. To update the website, this .js file then needs to be exported. On your browser, it is automatically loaded with the web pages. Additional Javascript functions then work in your browser to add the keywords visibly to the page, transform all tag spans into links, and provide a lookup function for the links to activate when a tag is clicked. They also add little touches like links to previous/next blog posts.

Indieweb features

RSS feed

We should bring back RSS feeds as a central way of keeping in touch with each other across personal websites. I have an RSS feed for the whole site as well as one for each language section. More could be added, e.g. a feed for the blog in each language or a feed only for the notes section, but I don’t see any point in it.

The Org exporter does not support RSS out of the box, and so neither does Org-publish, but adding it is quite simple. Bastien Guerry’s ox-rss.el exports an Org document as RSS XML. If you keep your blog in a single Org file with one entry (heading) per post, you can just export it as RSS as well as HTML. Since I keep each post as a separate file, and want one RSS feed to present an updated list of many recent posts, I need to keep some Org documents updated with such lists. Then, I configure a subproject in org-publish-project-alist to export them with ox-rss.el. The subproject looks something like this (Listing 5):

("RSS"
 :base-directory "~/www/cm/"
 :recursive t
 :preparation-function cm/write-newsfeed
 :base-extension "org"
 :rss-image-url "/img/cm.png"
 :html-link-home "https://christianmoe.com"
 :html-link-use-abs-url t
 :rss-extension "xml"
 :publishing-directory "/var/www/html"
 :publishing-function (org-rss-publish-to-rss)
 :section-numbers nil
 :exclude ".*"             ;; To exclude all files...
 :include ("rss-feed.org") ;; ... except named feeds
 :table-of-contents nil)

I could update the Org files manually. I like writing in Org, right? But it would become a chore that I’d probably forget or grow tired of, especially since for every post I’d want to update at least two feeds (one global, one for the language section).

Instead, I use a function (cm/write-newsfeed) to generate rss-feed.org pages from the metadata stored in cm/exported-files (see Keeping track of exported files and tags). The code looks something like this (Listing 6).

(defconst cm/rss-sections
  '("/" "/sl/" "/en/" "/nb/" "/eo/")
  "A list of sections of ChristianMoe.com for which RSS feeds
  will be generated automatically, each feed comprising files
  with paths that match a section.")

(defun cm/write-newsfeed (proplist)
  "Write newsfeed pages for the site, to be exported with
ox-rss. The required PROPLIST argument is unused."
  (message "Writing the news feed")
  (let ((files (copy-sequence cm/exported-files)) ; w/o copy-sequence, sort will screw up original list
        (sections cm/rss-sections)) ;; "" is the whole site
    ;; Sort files by date
    ;; FIXME: Inefficient to sort them as strings?
    (message "Sorting files\n")
    (setq files
          (sort files
                (lambda (a b)
                  (string<
                   (or (plist-get (cdr b) :date) "") ; empty string for nil dates
                   (or (plist-get (cdr a) :date) "")))))
    (message "Files sorted\n")
    ;; Make subfeed for each section and feed for whole site
    ;; (currently only each language sections)
      (let* ((c 0)
             (feed "")
             (section (car sections))
             ;; Filter files by section (language)
             (section-files
              (if (string= section "/") ;; If feed for whole site
                  files                    ;; just copy everything
                (filter (lambda (elem)     ;; else filter by section in paths
                          (string-match section (car elem)))
                        files))))
        (message "Starting section %s" section)
        ;; Add to each feed the most recent <= 50 files in the section
        (while (and section-files (< c 50))
          (let ((props (cdr (car section-files)))
                (path (car (car section-files))))
            (setq feed
                  (concat
                   feed
                   (format "\n* %s\n" (plist-get props :title))
                   (format      "  :PROPERTIES:\n  :PUBDATE: %s\n"
                                (plist-get props :date))
                   (format "  :RSS_PERMALINK: %s\n" path) ; or node num?
                   (format "  :END:\n\n%s\n"                            
                           (or (plist-get props :description) ""))))
            (setq c (+ c 1)
                  section-files (cdr section-files))))
      (write-region 
       (format "#+TITLE: RSS: ChristianMoe.com%s\n\n%s" section feed)
       nil
       (format "~/www/cm%srss-feed.org" section))
      (setq sections (cdr sections))))))

Since I’m automating this, I could just as well have written the function to produce the XML files directly, skipping the intermediary org step, but this saves me the bother of working with XML at all.

TODO Version control   Git

Instead on relying on whatever a CMS does for version control, I can just run Git in my source folder. Git would also be one way of publishing to a remote server, assuming one is allowed to set up a Git repository there. Git would also allow collaborative editing, although it would tend to limit your collaborators to technical types who know their way around the command line.

References

Leha, Andreas, and Tim Beißbarth. 2011. “The Emacs Org-Mode: Reproducible Research and beyond.” Warwick.

Colophon

© Christian Moe
2023-09-27
Some rights reserved.

Last changed:
2026-02-15

Published with Emacs 30.1 (Org mode 10.0-pre).

Valid XHTML 1.0 Strict

Validate