Blogging using org-mode (and nothing else)
Published: February 01, 2016
Update this blog post for an
updated org-sitemap-function
and an updated project definition. I'm
leaving this post up to preserve history.
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
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 gitThe custom Elisp sits in my emacs config. You can find a copy of the relevant sections below. (Update : 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)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:
![blogging_with_org_small.png](../img/blogging_with_org_small.png)
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' />\n" "<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 />\n <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>\n <p><a href= \"/contact.html\"> Contact</a></p>\n")
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) "\">\n" "<h6>" (capitalize name) "</h6>\n" content "\n</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) ;; loop through all of the files in the project (while (setq file (pop files)) (let ((fn (file-name-nondirectory file)) (link ;; changed this to fix links. see postprocessor. (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))) ;; sitemap shouldn't list itself (unless (equal (file-truename sitemap-filename) (file-truename file)) (let (;; get the title and date of the current file (title (org-publish-format-file-entry "%t" file project-plist)) (date (org-publish-format-file-entry "%d" file project-plist)) ;; get the preview section from the current file (preview (my-blog-get-preview file)) (regexp "\\(.*\\)\\[\\([^][]+\\)\\]\\(.*\\)")) ;; insert a horizontal line before every post, kill the first one ;; before saving (insert "-----\n") (cond ((string-match-p regexp title) (string-match regexp title) ;; insert every post as headline (insert (concat"* " (match-string 1 title) "[[file:" link "][" (match-string 2 title) "]]" (match-string 3 title) "\n"))) (t (insert (concat "* [[file:" link "][" title "]]\n")))) ;; add properties for `ox-rss.el' here (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 the date, preview, & read more link (insert (concat date "\n\n")) (insert preview) (insert (concat "[[file:" link "][Read More...]]\n")))))) ;; kill the first hrule to make this look OK (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) ;; massage the sitemap if wanted ;; delete the old file and write the correct one (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 ;; this enables htmlize, which means that I can use css for code! :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 ;; important!! ;; the following removes extra headers from HTML output -- important! :html-link-home "/" :html-head nil ;; cleans up anything that would have been in there. :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 ;; sitemap - list of blog articles :auto-sitemap t :sitemap-filename "blog.org" :sitemap-title "Blog" ;; custom sitemap generator function :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 ;; important!! :html-viewport nil ;; hasn't worked yet ;; the following removes extra headers from HTML output -- important! :html-link-home "/" :html-head nil ;; cleans up anything that would have been in there. :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.
(add-to-list 'org-structure-template-alist '("b" "#+TITLE: ? #+AUTHOR: Dennis Ogbe #+EMAIL: [email protected] #+DATE: #+STARTUP: showall #+STARTUP: inlineimages #+BEGIN_PREVIEW\n\n#+END_PREVIEW\n"))
If you'd like, you can let me know what you think. I appreciate any sorts of feedback.
Dennis