Basti's Scratchpad on the Internet

Blogging with Emacs

When I first started blogging, it was on blogger.com (on the now-abandoned domain daskrachen.com). On blogger, writing new posts (back then) involved typing raw HTML into a web form. Not what I would call ideal. This improved somewhat when they introduced a fancy rich text editor that would automatically transform beautiful text into a horrible formatting mess.

Thus I switched. Getting my blog posts out of blogger was… Let's just say that I lost anything I didn't have a plain-text backup of. And I switched to Pelican, a static site generator written in Python. It worked beautifully, until I updated something, at which point it resorted to just throwing errors. Now I don't have anything in particular against Python stack traces, but these particular traces traced deep into stuff that was (then?) too complex for me to understand.

Thus I switched again. This time to C()λ∈slaw■, a static site generator written in Common Lisp. Mainly because I was interested in Common Lisp at the time. It worked really well. However, this was supposed to give me a chance to delve into Common Lisp, and I failed to understand C()λ∈slaw■'s code. Realistically though, this is probably not C()λ∈slaw■'s fault. My knowledge of Common Lisp is far from perfect.

Thus it was time to switch again. Having been enamored with Emacs for the last few years, it made sense to blog with Emacs as well. Besides, I am kind of fed up with the many conflicting flavors of Markdown out there and have switched my personal note-taking to Org mode long ago. So let's set up Emacs and Org as a blogging platform!

Before we start though, a short disclaimer: This will be a very bare bones blogging engine. It will consist of some articles, a front page, an archive page, and an RSS feed. And you will have to manage the front page and RSS feed semi-manually. No tags, no fancy history. Just what you see here.

On the plus side, this will be implemented entirely within Emacs and very simple to understand. Writing a new blog post will be as simple as writing an Org file and hitting a key combination! And you will get all of Org's fancy syntax highlighting and export magic for free!

Getting the pages to work is rather simple: You have to create a "publishing project" that specifies a base-directory where your Org files live and a publishing-directory, where the HTML files are going to be stored. Since this is Emacs, you could make your publishing directory any TRAMP path you like and insta-publish your workings!

(BTW, I am using Org 8.2.2 and I believe you need at least 8.0 for these examples to work)

(require 'ox-html)
(require 'ox-rss)
(require 'ox-publish)
(setq org-publish-project-alist
      '(("blog-content"
         :base-directory "~/Projects/blog/posts"
         :html-extension "html"
         :base-extension "org"
         :publishing-directory "~/Projects/blog/publish"
         :publishing-function (org-html-publish-to-html)
         :recursive t          ; descend into sub-folders?
         :section-numbers nil  ; don't create numbered sections
         :with-toc nil         ; don't create a table of contents
         :with-latex t         ; do use MathJax for awesome formulas!
         :html-head-extra ""   ; extra <head> entries go here
         :html-preamble ""     ; this stuff is put before your post
         :html-postamble ""    ; this stuff is put after your post
)))

Now hit M-x org-publish, type in blog-content, and you have a blog! Awesome! We are done here.

Well, how about an archive page that lists all your previous blog entries?

Emacs can auto-generate this for you. Simply add these lines to blog-content:

:auto-sitemap t
:sitemap-filename "archive.org"
:sitemap-title "Archive"
:sitemap-sort-files anti-chronologically
:sitemap-style list
:makeindex t

Also, you can put something like

<a href="archive.html">Other posts</a>

into your :html-postamble to make every page link to this. You can also add your Disqus snippet there to enable comments.

Adding a front page is simple, too. My front page is simply a normal page called index.org, which contains links and slugs for every article I want to have on the front page. For example:

#+TITLE: RECENT POSTS

* Speeding up Matplotlib
#+include: "~/Projects/blog/posts/2013-05-30-speeding-up-matplotlib.org" :lines "4-9"
read more...

But a blog is more than just text. There are images and CSS, too. I keep all that stuff in a separate directory and use a separate publishing project to copy it over to the publishing directory. Just add to your publishing-alist:

("blog-static"
 :base-directory "~/Projects/blog/static"
 :base-extension "png\\|jpg\\|css"
 :publishing-directory "~/Projects/blog/publish/static"
 :recursive t
 :publishing-function org-publish-attachment)

Setting up the RSS feed works similarly. The RSS feed is created from a single Org file. Create a new publishing project and put it into your publishing-alist

("blog-rss"
 :base-directory "~/Projects/blog/posts"
 :base-extension "org"
 :publishing-directory "~/Projects/blog/publish"
 :publishing-function (org-rss-publish-to-rss)
 :html-link-home "http://bastibe.de/"
 :html-link-use-abs-url t
 :exclude ".*"
 :include ("rss.org")
 :with-toc nil
 :section-numbers nil
 :title "Bastis Scratchpad on the Internet")

Make sure to exclude this rss.org from the blog-content project by adding it's name to the :exclude variable though. This rss.org file should contain headlines for every blog post. Every headline needs a publishing date and a permalink as property and the body of the post as content:

* Speeding up Matplotlib
:PROPERTIES:
:RSS_PERMALINK: "http://bastibe.de/2013-05-30-speeding-up-matplotlib.html"
:PUBDATE: <2013-05-30>
:END:
#+include: "~/Projects/blog/posts/2013-05-30-speeding-up-matplotlib.org" :lines "4-"

I exclude the first three lines, since they only contain #+title, #+date, and #+tags. You should at least exclude the #+title line. Otherwise, ox-rss will get confused about which title to choose for the feed.

You can even create a meta publishing project that executes all three projects in one fell swoop!

("blog"
 :components ("blog-content" "blog-static" "blog-rss"))

There is one more thing that is kind of fiddly though: As I said, I use Disqus for comments, but I don't want to have comment boxes on the front page or the archive. Thankfully though, ox-html allows you to set :html-preamble and :html-postamble to a function, in which case that function can decide what pre/postamble to draw! The function can take an optional argument that contains a plist of article metadata. In this case, I decide on the :title metadata whether to print the archive link and Disqus, only the archive link, or neither:

:html-postamble
(lambda (info)
  "Do not show disqus for Archive and Recent Posts"
  (cond ((string= (car (plist-get info :title)) "Archive")
         "")
        ((string= (car (plist-get info :title)) "Recent Posts")
         "<div id=\"archive\"><a href=\"archive.html\">Other posts</a></div>")
        (t
    "<div id=\"archive\"><a href=\"archive.html\">Other posts</a></div>
     <div id=\"disqus_thread\"></div>
     <script type=\"text/javascript\">
     ..."

This should get you started! For completeness, here is my complete configuration:

(require 'ox-html)
(require 'ox-rss)
(require 'ox-publish)
(setq org-publish-project-alist
      '(("blog"
         :components ("blog-content" "blog-static" "blog-rss"))
        ("blog-content"
         :base-directory "~/Projects/blog/posts"
         :html-extension "html"
         :base-extension "org"
         :publishing-directory "~/Projects/blog/publish"
         :publishing-function (org-html-publish-to-html)
         :auto-sitemap t
         :sitemap-filename "archive.org"
         :sitemap-title "Archive"
         :sitemap-sort-files anti-chronologically
         :sitemap-style list
         :makeindex t
         :recursive t
         :section-numbers nil
         :with-toc nil
         :with-latex t
         :html-head-include-default-style nil
         :html-head-include-scripts nil
         :html-head-extra
         "<link rel=\"alternate\" type=\"appliation/rss+xml\"
                href=\"http://bastibe.de/rss.xml\"
                title=\"RSS feed for bastibe.de\">
          <link href='http://fonts.googleapis.com/css?family=Roboto&subset=latin' rel='stylesheet' type='text/css'>
          <link href='http://fonts.googleapis.com/css?family=Ubuntu+Mono' rel='stylesheet' type='text/css'>
          <link href= \"static/style.css\" rel=\"stylesheet\" type=\"text/css\" />
          <title>Basti's Scratchpad on the Internet</title>
          <meta http-equiv=\"content-type\" content=\"application/xhtml+xml; charset=UTF-8\" />
          <meta name=\"viewport\" content=\"initial-scale=1,width=device-width,minimum-scale=1\">"
         :html-preamble
         "<div class=\"header\">
              <a href=\"http://bastibe.de\">Basti's Scratchpad on the Internet</a>
              <div class=\"sitelinks\">
                  <a href=\"http://alpha.app.net/bastibe\">alpha.app.net</a>  | <a href=\"http://github.com/bastibe\">Github</a>
              </div>
          </div>"
         :html-postamble
         (lambda (info)
           "Do not show disqus for Archive and Recent Posts"
           (cond ((string= (car (plist-get info :title)) "Archive") "")
                 ((string= (car (plist-get info :title)) "Recent Posts")
                  "<div id=\"archive\"><a href=\"archive.html\">Other posts</a></div>")
                 (t
             "<div id=\"archive\"><a href=\"archive.html\">Other posts</a></div>
              <div id=\"disqus_thread\"></div>
              <script type=\"text/javascript\">
              /* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */
              var disqus_shortname = 'bastibe';
              /* * * DON'T EDIT BELOW THIS LINE * * */
              (function() {
                var dsq = document.createElement('script');
                dsq.type = 'text/javascript';
                dsq.async = true;
                dsq.src = 'http://' + disqus_shortname + '.disqus.com/embed.js';
                (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
                  })();
              </script>
              <noscript>Please enable JavaScript to view the
                  <a href=\"http://disqus.com/?ref_noscript\">comments powered by Disqus.</a></noscript>
              <a href=\"http://disqus.com\" class=\"dsq-brlink\">comments powered by <span class=\"logo-disqus\">Disqus</span></a>")))
         :exclude "rss.org\\|archive.org\\|theindex.org")
        ("blog-rss"
         :base-directory "~/Projects/blog/posts"
         :base-extension "org"
         :publishing-directory "~/Projects/blog/publish"
         :publishing-function (org-rss-publish-to-rss)
         :html-link-home "http://bastibe.de/"
         :html-link-use-abs-url t
         :exclude ".*"
         :include ("rss.org")
         :with-toc nil
         :section-numbers nil
         :title "Bastis Scratchpad on the Internet")
        ("blog-static"
         :base-directory "~/Projects/blog/static"
         :base-extension "png\\|jpg\\|css"
         :publishing-directory "~/Projects/blog/publish/static"
         :recursive t
         :publishing-function org-publish-attachment)))

All other sources, including the source code to all blog posts, can be found on Github (the master branch contains HTML, the source branch contains Org).

Addendum: I have since discovered that org-rss-publish-to-rss only handles top-level headlines, but disregards second-level or higher-level headlines. Thus, if you have a post with nested headlines, your RSS feed will only include the text of the top-level one. To fix this, I advised org-rss-publish-to-rss to use org-html-headline for non-top-level headlines like this:

(defadvice org-rss-headline
  (around my-rss-headline (headline contents info) activate)
  "only use org-rss-headline for top level headlines"
  (if (< (org-export-get-relative-level headline info) 2)
      ad-do-it
    (setq ad-return-value (org-html-headline headline contents info))))

Now, the RSS feed includes the full text of all articles.

Other posts
comments powered by Disqus