Blogging using org-mode (and nothing else)

As you can tell, the look of this website has changed significantly—and it was about time for that. In case you didn't know, this site used to be hosted on http://web.ics.purdue.edu/, which provides free webspace for Purdue students. I used to generate the static HTML pages from plaintext markdown files using the Python-based static site generator Pelican. It worked well for a while, but I ended up having a few issues with that setup: The host was painstakingly slow to reach from anywhere but the Purdue networks

Pelican would just break sometimes, providing me with nothing but some cryptic Python exception messages

The website did not use TLS and loaded a lot of external content over an unencrypted HTTP connection, causing it to render incompletely when using HTTPS Everywhere

I prefer Org-mode over the fragmented Markdown syntax for writing plaintext documents

Source code blocks are prettier in Org-mode So, of course, I looked for an alternative.

Since I was predisposed towards using Org-mode for this, Org's publishing feature was the first alternative I investigated. I don't have high demands—all I need is a lightweight, stable, static site generator. I don't need tag clouds, sophisticated pagination, theme support (I'm fine with hacking together my own CSS), or any kind of plugin support; all I really need is a small org-to-HTML converter that can be hacked using Elisp and that I can bolt the extras that I want onto. After browsing around a little bit, I found some neat examples here, here and here. These sites are exactly what I wanted—minimalist, simple, and based on pure Org.

Obviously, the Org publishing feature was all that I needed. I whipped up a nice little configuration that produces this website from a set of Org source files, some custom CSS and HTML, and some custom Elisp. This is what happens at a high level:

All of the blog's files sit in the ~/repos/blog directory and are version controlled using git

directory and are version controlled using git The custom Elisp sits in my emacs config. You can find a copy of the relevant sections below. ( Update : This has moved to here)

This has moved to here) The source files can be roughly divided into the following categories: /blog/ —Each blog post is contained in an individual Org-file /pages/ —Static Pages, like the landing page, sit in their own directory as Org files /res/ —Contains custom CSS and the MathJax JavaScript

Org's publishing function org-publish uses magic (and some Lisp) to spit out simple, easy to read, and easy to render HTML from these sources (and a few others)

uses magic (and some Lisp) to spit out simple, easy to read, and easy to render HTML from these sources (and a few others) After the HTML files are generated, I'm using rsync to push them onto a Tec-X1 instance from bladetec, which runs nginx on Ubuntu 14.04 and costs a phenomenal €0.99 a month

Here's the obligatory screenshot of me editing this post in Emacs:

There are a few tricks involved in doing this—mostly concerning the generation of the sitemap—, but I will let the relevant section of my config (Update : This has moved to here) speak for themselves, since they're pretty well commented (I think):

UPDATE :

I've received a few questions asking whether there exists an RSS feed for this blog. There wasn't until today, but I think this will work. I'm using ox-rss.el to generate XML from the blog's sitemap. I had to trick it into doing a few things like generating the correct pubdates and permalinks, but I think it works fine for what I need. The code below is the updated version, with RSS.

The publishing uses the Org HTML export backend a lot, so to start off, we require it here, along with the RSS publishing backend.

( require ' ox-html ) ( require ' ox-rss ) ( setq org-export-html-coding-system 'utf-8-unix) ( setq org-html-viewport nil)

Next, we define some functions and variables that will be used by org-publish . First, let's define the website headers, footers, and make sure that the exported HTML points to the right style sheets.

( setq my-blog-extra-head (concat "<link rel='stylesheet' href='/../res/code.css' />

" "<link rel='stylesheet' href='/../res/main.css' />" )) ( setq my-blog-header-file "~/repos/blog/header.html" ) ( defun my-blog-header (arg) ( with-temp-buffer (insert-file-contents my-blog-header-file) (buffer-string))) ( setq my-blog-footer "<hr />

<p><span style=\"float: left;\"><a href= \"/blog.xml\">RSS</a></span> License: <a href= \"https://creativecommons.org/licenses/by-sa/4.0/\">CC BY-SA 4.0</a></p>

<p><a href= \"/contact.html\"> Contact</a></p>

" )

I'd also like to export drawers out to HTML; this idea is ripped directly from here.

( defun my-blog-org-export-format-drawer (name content) (concat "<div class=\"drawer " (downcase name) "\">

" "<h6>" (capitalize name) "</h6>

" content "

</div>" ))

MathJax usually recommends to use their CDN to load their JavaScript code, but I want to use a version that sits on my server.

( setq my-blog-local-mathjax '((path "/res/mj/MathJax.js?config=TeX-AMS-MML_HTMLorMML" ) (scale "100" ) (align "center" ) (indent "2em" ) (tagside "right" ) (mathml nil)))

Now we'll get to some of the customizations I've bolted on Org's publishing features. In it's standard configuration, the sitemap generator produces a plain, kind of boring looking list of posts, which was inadequate for me. After hacking on the sitemap generation function for a little while, I came up with the following solution: When I write a blog post, I enclose the "preview" part of the post in #+BEGIN_PREVIEW...#+END_PREVIEW tags, which my (very simple) parser then inserts into the sitemap page.

( defun my-blog-get-preview (file) "The comments in FILE have to be on their own lines, prefereably before and after paragraphs." ( with-temp-buffer (insert-file-contents file) (goto-char (point-min)) ( let ((beg (+ 1 (re-search-forward "^#\\+BEGIN_PREVIEW$" ))) (end ( progn (re-search-forward "^#\\+END_PREVIEW$" ) (match-beginning 0)))) (buffer-substring beg end)))) ( defun my-blog-sitemap (project &optional sitemap-filename) "Generate the sitemap for my blog." ( let* ((project-plist (cdr project)) (dir (file-name-as-directory (plist-get project-plist :base-directory ))) (localdir (file-name-directory dir)) (exclude-regexp (plist-get project-plist :exclude )) (files (nreverse (org-publish-get-base-files project exclude-regexp))) (sitemap-filename (concat dir ( or sitemap-filename "sitemap.org" ))) (sitemap-sans-extension (plist-get project-plist :sitemap-sans-extension )) (visiting (find-buffer-visiting sitemap-filename)) file sitemap-buffer) ( with-current-buffer ( let ((org-inhibit-startup t)) ( setq sitemap-buffer ( or visiting (find-file sitemap-filename)))) (erase-buffer) ( while ( setq file ( pop files)) ( let ((fn (file-name-nondirectory file)) (link (file-relative-name file (file-name-as-directory (expand-file-name (concat (file-name-as-directory dir) ".." ))))) (oldlocal localdir)) ( when sitemap-sans-extension ( setq link (file-name-sans-extension link))) ( unless (equal (file-truename sitemap-filename) (file-truename file)) ( let ( (title (org-publish-format-file-entry "%t" file project-plist)) (date (org-publish-format-file-entry "%d" file project-plist)) (preview (my-blog-get-preview file)) (regexp " \\ ( .* \\ ) \\[ \\ ( [ ^ ][]+ \\ ) \\] \\ ( .* \\ ) " )) (insert "-----

" ) ( cond ((string-match-p regexp title) (string-match regexp title) (insert (concat "* " (match-string 1 title) "[[file:" link "][" (match-string 2 title) "]]" (match-string 3 title) "

" ))) (t (insert (concat "* [[file:" link "][" title "]]

" )))) ( let ((rss-permalink (concat (file-name-sans-extension link) ".html" )) (rss-pubdate (format-time-string (car org-time-stamp-formats) (org-publish-find-date file)))) (org-set-property "RSS_PERMALINK" rss-permalink) (org-set-property "PUBDATE" rss-pubdate)) (insert (concat date "



" )) (insert preview) (insert (concat "[[file:" link "][Read More...]]

" )))))) (goto-char (point-min)) ( let ((kill-whole-line t)) (kill-line)) (save-buffer)) ( or visiting (kill-buffer sitemap-buffer))))

Next I define some pre-and postprocessors that run during the publishing process. They are used to move around some files before and after publishing.

( setq my-blog-emacs-config-name "emacsconfig.org" ) ( setq my-blog-process-emacs-config nil) ( defun my-blog-pages-preprocessor () "Move a fresh version of the settings.org file to the pages directory." ( when my-blog-process-emacs-config ( let* ((cfg-file (expand-file-name (concat (file-name-as-directory user-emacs-directory) "settings.org" ))) (destdir (file-name-as-directory (plist-get project-plist :base-directory ))) (cfg-file-dest (expand-file-name (concat destdir my-blog-emacs-config-name)))) (copy-file cfg-file cfg-file-dest t)))) ( defun my-blog-pages-postprocessor () (message "In the pages postprocessor." )) ( defun my-blog-articles-preprocessor () (message "In the articles preprocessor." )) ( defun my-blog-articles-postprocessor () "Massage the sitemap file and move it up one directory. for this to work, we have already fixed the creation of the relative link in the sitemap-publish function" ( let* ((sitemap-fn (concat (file-name-sans-extension (plist-get project-plist :sitemap-filename )) ".html" )) (sitemap-olddir (plist-get project-plist :publishing-directory )) (sitemap-newdir (expand-file-name (concat (file-name-as-directory sitemap-olddir) ".." ))) (sitemap-oldfile (expand-file-name sitemap-fn sitemap-olddir)) (sitemap-newfile (expand-file-name (concat (file-name-as-directory sitemap-newdir) sitemap-fn)))) ( with-temp-buffer (goto-char (point-min)) (insert-file-contents sitemap-oldfile) (delete-file sitemap-oldfile) (write-file sitemap-newfile))))

The next preprocessor runs CSSTidy on the site's CSS.

( defun my-blog-minify-css () ( let* ((csstidy "csstidy" ) (csstidy-args " --template=highest --silent=true" ) (css-dir (expand-file-name (plist-get project-plist :publishing-directory ))) (css-files (directory-files css-dir t "^.*\\.css$" ))) ( dolist (file css-files) ( with-temp-buffer (insert (shell-command-to-string (concat csstidy " " file csstidy-args))) (write-file file)))))

Most of the publishing settings are defined in org-publish-project-alist .

( setq org-publish-project-alist `(( "blog" :components ( "blog-articles" , "blog-pages" , "blog-rss" , "blog-res" , "blog-images" , "blog-dl" )) ( "blog-articles" :base-directory "~/repos/blog/blog/" :base-extension "org" :publishing-directory "~/repos/blog/www/blog/" :publishing-function org-html-publish-to-html :preparation-function my-blog-articles-preprocessor :completion-function my-blog-articles-postprocessor :htmlized-source t :with-author t :with-creator nil :with-date t :headline-level 4 :section-numbers nil :with-toc nil :with-drawers t :with-sub-superscript nil :html-link-home "/" :html-head nil :html-head-extra ,my-blog-extra-head :html-head-include-default-style nil :html-head-include-scripts nil :html-viewport nil :html-format-drawer-function my-blog-org-export-format-drawer :html-home/up-format "" :html-mathjax-options ,my-blog-local-mathjax :html-mathjax-template "<script type=\"text/javascript\" src=\"%PATH\"></script>" :html-footnotes-section "<div id='footnotes'><!--%s-->%s</div>" :html-link-up "" :html-link-home "" :html-preamble my-blog-header :html-postamble ,my-blog-footer :auto-sitemap t :sitemap-filename "blog.org" :sitemap-title "Blog" :sitemap-function my-blog-sitemap :sitemap-sort-files anti-chronologically :sitemap-date-format "Published: %a %b %d %Y" ) ( "blog-pages" :base-directory "~/repos/blog/pages/" :base-extension "org" :publishing-directory "~/repos/blog/www/" :publishing-function org-html-publish-to-html :preparation-function my-blog-pages-preprocessor :completion-function my-blog-pages-postprocessor :htmlized-source t :with-author t :with-creator nil :with-date t :headline-level 4 :section-numbers nil :with-toc nil :with-drawers t :with-sub-superscript nil :html-viewport nil :html-link-home "/" :html-head nil :html-head-extra ,my-blog-extra-head :html-head-include-default-style nil :html-head-include-scripts nil :html-format-drawer-function my-blog-org-export-format-drawer :html-home/up-format "" :html-mathjax-options ,my-blog-local-mathjax :html-mathjax-template "<script type=\"text/javascript\" src=\"%PATH\"></script>" :html-footnotes-section "<div id='footnotes'><!--%s-->%s</div>" :html-link-up "" :html-link-home "" :html-preamble my-blog-header :html-postamble ,my-blog-footer) ( "blog-rss" :base-directory "~/repos/blog/blog/" :base-extension "org" :publishing-directory "~/repos/blog/www/" :publishing-function org-rss-publish-to-rss :html-link-home "https://ogbe.net/" :html-link-use-abs-url t :title "Dennis Ogbe" :rss-image-url "https://ogbe.loc/img/feed-icon-28x28.png" :section-numbers nil :exclude ".*" :include ( "blog.org" ) :table-of-contents nil) ( "blog-res" :base-directory "~/repos/blog/res/" :base-extension ".*" :publishing-directory "~/repos/blog/www/res/" :publishing-function org-publish-attachment :completion-function my-blog-minify-css) ( "blog-images" :base-directory "~/repos/blog/img/" :base-extension ".*" :publishing-directory "~/repos/blog/www/img/" :publishing-function org-publish-attachment :recursive t) ( "blog-dl" :base-directory "~/repos/blog/dl/" :base-extension ".*" :publishing-directory "~/repos/blog/www/dl/" :publishing-function org-publish-attachment :Recursive t)))

Finally, define a small template for new blog posts.

If you'd like, you can let me know what you think. I appreciate any sorts of feedback.

Dennis