How to write a blog with Emacs

11 Jan 2023

How simple should be writing a blog post for a developer? I would say 4 passages; write, save, build, publish. That’s exaclty the number of steps it took me with this article. I wrote and converted it to a webpage using Emacs and I will talk about how I configured my favourite editor to make as simple as possible publishing a blog.

This are the steps I’ve follow with Emacs to write and publish a post:

  1. Create a new org-mode buffer with C-x b and write the post
  2. Save the post in the correct folder C-x C-s
  3. Run the build process with M-x org-publish and selected blog-publish
  4. Push the public/ folder to the git repo using magit

This blog is auto deployed by just updating the repository, I’ll speak about that in a future article. For now let’s have a look at the Emacs configuration that I used. You can find the source code here.

If you never used Emacs before, you should try Doom Emacs. Is a preset configuration that has all included to start with.

The initial configuration can be challenging if you’re not comfortable with Emacs customisation using elisp. But for Emacs users, extending it is for sure the funniest part.

Why using Emacs

First, I like writing using the org markup language. I think it’s way more powerful than the standard markdown since it supports more features1 with a lot of customisation and functionalities thanks to Emacs. Markdown is quite limited and the standard way to extending it consists of using directly HTML, which means adding another markup language. With org you don’t need to do that, just use one of the multiple templates offered by the language. Second, since Emacs is highly customisable, I created my perfect writing setup combining extensions like olivetti-mode, using the ETBook font, a dark theme and disabling some features like line numbers. Third is because is so simple to just write a post, build and push the repository all directly from Emacs that no other solutions I’ve tried before can emulate.

The generated website uses only html5 and css, but you can extend it as much as you want, adding js or other features. Is like having hugo or jekyll directly intergrated into your text editor, plus you can customise the text transformation and the page generation. You’re in full control of the process.

Folder structure

The folder structure used for this blog is pretty simple.

  • src/ for the source code to be used by the build process
    • css/ for the stylesheets
    • js/ for the js (now still empty)
    • img/ for the images
    • layouts/ for reusable portions of html
    • posts/ for the org files used as posts, plus the file
    • other pages as and
  • public/ is for the output folder of the build process
  • blog.el where I keep my Emacs configuration for the blog

Emacs configuration

The following is a closer look to the blog.el file that enable Emacs to build the website with only one command. For the one of you that are unfamiliar with extending Emacs, it uses it’s own Lisp implementation called elisp. If you ever used any Lisp dialect, you will get familiar with it immediately. Two good things to know to start using it are:

  • evaluates any expressions with eval-last-sexp C-x C-e
  • lookups for documentation with the cursor on the function with C-c c k
  • shows the source code of a function use find-fuction.

First le’ts require two Emacs features that I’m going to use. They will import all functions that I need for generating the website files and to create the XML file for RSS.

(require 'ox-publish)
(require 'ox-rss)


First I defined a configuration for the blog called… blog-conf. The key-values are pretty straitforward, :src is from the sources path and :out for the output one. The variable type is a property list2.

(defvar blog-conf
  '(:title              "What the dormouse said?"
    :desc               "The White Rabbit's blog"
    :author             "White Rabbit"
    :url                ""
    :src                "~/org/blog/src"
    :out                "~/org/blog/public"
    :layouts            "~/org/blog/src/layouts"
    :src-posts          "~/org/blog/src/posts"
    :out-posts          "~/org/blog/public/posts")
  "Configuration for exporting project.")

I then defined a function to easily extract a configuration value using only the key, from the property list. All definitions into the blog elisp file should follow the naming convention of including the file name at the beginning, so since the file is called blog.el, all functions/variables have to start with blog-.

(defun blog-getconf (k)
  "Return the configuration value with key K extracted from conf alist."
  (plist-get blog-conf k))

To setup a new org-publish project, is necessary to add it to a specific an association-list called org-publish-project-alist. I specified first a list of different steps with their own configuration, then to group them toghether in a sequence in order to use a single command and publish them all.

Here there’s the first entry called org, used to configure the automatic conversion in HTML of all the .org files in the src/ directory. It’s not recursive, so subfolders would not be processed.

(setq org-publish-project-alist
         :base-directory        ,(blog-getconf :src)
         :base-extension        "org"
         :publishing-directory  ,(blog-getconf :out)
         :exclude               "README\\.md\\|LICENSE\\|\\.gitignore\\|rss\\.org"
         :publishing-function   org-html-publish-to-html
         :html-link-home        "index.html"
         :html-home/up-format   ""
         :recursive             nil
         ; HTML5
         :html-extension        "html"
         :htmlized-source       t
         :html-doctype          "html5"
         :html-html5-fancy      t
         ;; Extra
         :language              "en"
         :section-numbers       nil
         :with-toc              nil
         :with-date             nil
         :headlinevels          4
         :html-toplevel-hlevel  1
         :with-email            nil
         :with-title            nil
         ;; Disable some Org's HTML defaults
         :html-head-include-scripts       nil
         :html-head-include-default-style nil
         ;; HTML
         :html-head             ,(blog-read-layout-file 'head)
         :html-postamble        t
         :html-preamble-format  ,(blog-layout-format 'preamble-head)
         :html-postamble-format ,(blog-layout-format 'postamble)
         ;; Meta
         :title                 ,(blog-getconf :title)
         :author                ,(blog-getconf :author)
         :email                 ""
         ;; :meta-image            nil
         :meta-type             "website")

setq sets the value of the symbol org-publish-project-alist to a new association list, where the first element "org" is the name of the entry. The keys-values used are mostly self-explanatory and you can refer to the publishing options manual page for more information. What’s important here is to exclude some files from being processed and I don’t use a recursive strategy with subfolders because the rest of the .org files will be converted with other projects. I also added three custom layout files (head, preamble-head and postamble) to each page, that have been read as strings by the function blog-read-layout-file and formatted by blog-layout-format. Both this functions and the layout files will be shown later.

To continue with the list of entries, posts converts all the .org post files from the directory posts/ to a directory with the same name (as :out-posts config). The difference from before is that it generates a sitemap called and then will convert it into HTML when included into file. To generate this sitemap it will use two other functions: blog-sitemap and blog-sitemap-entry, both to extract data and format it. I’m also using a different preamble layout file and a different :meta-type for the generated pages.

         :base-directory        ,(blog-getconf :src-posts)
         :base-extension        "org"
         :publishing-directory  ,(blog-getconf :out-posts)
         :exclude               "rss\\.org"
         :publishing-function   blog-puhlish-to-html
         :html-link-home        "index.html"
         :html-home/up-format   ""
         ; HTML5
         :html-extension        "html"
         :htmlized-source       t
         :html-doctype          "html5"
         :html-html5-fancy      t
         ;; Extra
         :language              "en"
         :section-numbers       nil
         :with-toc              nil
         :with-date             nil
         :headline-levels       4
         :html-toplevel-hlevel  1
         :with-email            nil
         :with-title            t
         ;; Disable some Org's HTML defaults
         :html-head-include-scripts       nil
         :html-head-include-default-style nil
         ;; HTML
         :html-head             ,(blog-read-layout-file 'head)
         :html-preamble         t
         :html-postamble        t
         :html-preamble-format  ,(blog-layout-format 'preamble)
         :html-postamble-format ,(blog-layout-format 'postamble)
         ;; Sitemap
         :auto-sitemap          t
         :sitemap-date-format   "Published: %a %b %d %Y"
         :sitemap-filename      ""
         :sitemap-title         ,(blog-getconf :title)
         :sitemap-sort-files    anti-chronologically
         :sitemap-function      blog-sitemap
         :sitemap-format-entry  blog-sitemap-entry
         ;; Meta
         :title                 ,(blog-getconf :title)
         :author                ,(blog-getconf :author)
         :email                 ""
         :meta-type             "article")

The following entries are used to clone assets files into thje :out directory using the function org-publish-attachment. Each of this assets has is own folder, that’s why I made a separate entry for each of them.

         :base-directory        ,(expand-file-name "img" (blog-getconf :src))
         :base-extension        "jpg\\|jpeg\\|gif\\|png\\|svg"
         :publishing-directory  ,(expand-file-name "img" (blog-getconf :out))
         :publishing-function   org-publish-attachment
         :recursive             t)

         :base-directory        ,(expand-file-name "js" (blog-getconf :src))
         :base-extension        "js"
         :publishing-directory  ,(expand-file-name "js" (blog-getconf :out))
         :publishing-function   org-publish-attachment)

         :base-directory        ,(expand-file-name "css" (blog-getconf :src))
         :base-extension        "css"
         :publishing-directory  ,(expand-file-name "css" (blog-getconf :out))
         :publishing-function   org-publish-attachment
         :recursive             t)

         :base-directory        ,(expand-file-name "fonts" (blog-getconf :src))
         :base-extension        "eot\\|svg\\|ttf\\|woff\\|woff2"
         :publishing-directory  ,(expand-file-name "fonts" (blog-getconf :out))
         :publishing-function   org-publish-attachment
         :recursive             t)

This entry is used to generate a XML file for the blog RSS. It has been a bit tricky to configure since it wasn’t generating properly the file. To do this I’m using the ox-rss extension which can be found here and have to be loaded into Emacs. I’m using three custom functions for generate the RSS, blog-posts-rss-feed, blog-format-rss-feed and blog-format-rss-feed-entry that we will see below. Also is important to exclude some files to be indexed into from RSS feed.

         :base-directory                ,(blog-getconf :src-posts)
         :base-extension                "org"
         :publishing-directory          ,(blog-getconf :out)
         :publishing-function           blog-posts-rss-feed
         :recursive                     nil
         :exclude                       ,(regexp-opt '(""
        :html-link-home                ,(blog-getconf :url)
         :rss-extension                 "xml"
         :html-link-use-abs-url         t
         :html-link-org-files-as-html   t
         ;; Sitemap
         :auto-sitemap                  t
         :sitemap-filename              ""
         :sitemap-title                 ,(blog-getconf :title)
         :sitemap-style                 list
         :sitemap-sort-files            anti-chronologically
         :sitemap-function              blog-format-rss-feed
         :sitemap-format-entry          blog-format-rss-feed-entry
         ;; Meta
         :author                        ,(blog-getconf :author)
         :email                         "")

Last, the most important entry that group a sequence of steps for publishing the entire project. Now when using with org-publish to generate the website, I’m able to select blog-publish and Emacs will exports my blog project into a website.

        ("blog-publish" :components ("org" "posts" "img"
                                     "js" "css" "fonts" "rss"))))

Layout functions

This function reads a layout file as a string. It reads the content of the input file and returns it as a string, using a temporary buffer. Layouts are html files in the src/layouts/ folder.

(defun blog-read-layout-file (filename)
  "Return the content for the layout file of name FILE-NAME."
  (let ((layout-dir (expand-file-name (blog-getconf :layouts))))
       (insert-file-contents (expand-file-name (format "%s.html" filename) layout-dir))

The layouts for preable/postamble instead need to be formatted in a particular way, like a list with the first element the language code (like “en”) and the second item the content of the layout file. This function does exactly that.

(defun blog-layout-format (filename)
  "Return a layout file content formatted for preable/postamble."
  `(("en" ,(blog-read-layout-file filename))))

RSS functions

This three functions below are used to generate the RSS file called inside posts/ following the configuration above. blog-format-rss-feed writes the head of the file including only the title of the blog. org-list-to-subtree creates a new org list element from the list argument, containing all the posts. blog-format-rss-feed-entry generates a string for each single entry in the list, with title and some properties as permalink and date. blog-posts-rss-feed just adds a check to avoid processing other posts files except

Regarding the function parameters, from now on remember that plist contains the configuration properties, filename the source file and pub-dir the output directory.

(defun blog-format-rss-feed (title list)
  "Generate RSS feed, as a string.
 TITLE is the title of the RSS feed. LIST is an internal
 representation for the files to include, as returned by
 `org-list-to-lisp'.  PROJECT is the current project."
  (concat "#+TITLE: " title "\n\n" (org-list-to-subtree list)))

(defun blog-format-rss-feed-entry (entry style project)
  "Format ENTRY for the RSS feed.
 ENTRY is a file name.  STYLE is either 'list' or 'tree'.
 PROJECT is the current project."
  (let* ((title (org-publish-find-title entry project))
         (link (concat (file-name-sans-extension entry) ".html"))
         (pubdate (format-time-string (car org-time-stamp-formats)
                   (org-publish-find-date entry project))))
    (format "%s
     :rss_permalink: %s
     :pubdate: %s
     :END:\n" title link pubdate)))

(defun blog-posts-rss-feed (plist filename dir)
  "Publish PLIST to RSS when FILENAME is
 DIR is the location of the output."
  (if (equal "" (file-name-nondirectory filename))
      (org-rss-publish-to-rss plist filename dir)))

Sitemaps functions

All the functions used to generate the sitemap are here. The sitemap file path is src/posts/ and it should already be created but empty. The function blog-format-date-subtitle returns the publish date of the file in a nice format. blog-sitemap-entry format the post list entry with title, link and date. blog-sitemap instead define the sitemap file with title, options, meta, description and add a special attribute with ATTR_HTML defining the css class of the element as a string sitemap. This is used to style the posts list in the homepage, in a different way from other lists. The list of posts is getting filtered to remove invalid entries like hidden pages or 404 page.

(defun blog-format-date-subtitle (file project)
  "Format the date found in FILE of PROJECT."
  (let ((date (org-publish-find-date file project)))
    (string-trim (format-time-string "%e %b %Y" date))))

(defun blog-sitemap-entry (entry style project)
  "Format for sitemap ENTRY, as a string.
  ENTRY is a file name.  STYLE is the default style of the sitemap.
  PROJECT is the current project."
  (unless (or (equal entry "")
              (string-prefix-p "_" entry))
    (format "[[file:%s][%s]] /%s/"
            (org-publish-find-title entry project)
            (blog-format-date-subtitle entry project))))

(defun blog-sitemap (title list)
  "Generate sitemap as a string, having TITLE.
 LIST is an internal representation for the files to include, as
 returned by `org-list-to-lisp'."
  (let ((filtered-list (cl-remove-if (lambda (x)
                                       (and (sequencep x) (null (car x))))
    (concat "#+TITLE: " title "\n"
            "#+OPTIONS: title:nil\n"
            "#+META_TYPE: website\n"
            "#+DESCRIPTION: " (blog-getconf :desc) "\n"
            "\n#+ATTR_HTML: :class sitemap\n"
            (org-list-to-org filtered-list))))

Publish functions

I added a function to create a self closing html tag 3. It receives the tag name and multiple lists of attributes with name-value. For example calling it with (blog-html-tag "link" '(href ""')) will produce a string as <link href="">.

(defun blog-html-tag (name &rest attrs)
  "Returns a self closing html tag for string NAME.
 ATTRS specify additional attributes as lists."
  (let* ((format-f (lambda (attr)
                     (format "%s=\"%s\"" (car attr) (cadr attr))))
         (tag-attrs (mapconcat format-f  attrs " ")))
    (concat "<" name " " tag-attrs ">")))

I’m using the function above to generate meta tags to add into each page using the following code. Essentially it extracts information from the page and the project definition and put everything into a proper tags. Useful for SEO. org-publish-find-property find the property with the key in the project.

(defun blog-html-head-extra (file project)
  "Return <meta> elements for SEO.
 FILE is the html filename, PROJECT contains the properties."
  (let* ((info (cdr project))
          `((:title "TITLE" nil nil parse)
            (:date "DATE" nil nil parse)
            (:author "AUTHOR" nil ,(plist-get info :author) space)
            (:description "DESCRIPTION" nil nil newline)
            (:keywords "KEYWORDS" nil nil space)
            (:meta-image "META_IMAGE" nil ,(plist-get info :meta-image) nil)
            (:meta-type "META_TYPE" nil ,(plist-get info :meta-type) nil)))
         (project-title (org-publish-find-title file project))
         (title (if (not (string= project-title "Title"))
                  (blog-getconf :title)))
         (date (org-publish-find-date file project))
         (author (org-publish-find-property file :author project))
         (description (org-publish-find-property file :description project))
         (link-home (file-name-as-directory (plist-get info :html-link-home)))
         (extension (or (plist-get info :html-extension) org-html-extension))
         (rel-file (org-publish-file-relative-name file info))
         (full-url (concat link-home (file-name-sans-extension rel-file) "." extension))
         (meta-img (org-publish-find-property file :meta-image project))
         (image (when meta-img (concat link-home meta-img)))
         (favicon "/img/favicon.ico")
         (type (org-publish-find-property file :meta-type project)))
    (mapconcat 'identity
               `(,(blog-html-tag "link" '(rel icon) '(type image/x-icon) `(href ,favicon))
                 ,(blog-html-tag "link" '(rel alternate
                                          '(type application/rss+xml)
                                          '(href "rss.xml")
                                          '(title "RSS feed")))
                 ,(blog-html-tag "meta" '(property og:title) `(content ,title))
                 ,(blog-html-tag "meta" '(property og:url) `(content ,full-url))
                 ,(and description
                       (blog-html-tag "meta" '(property og:description) `(content ,description)))
                 ,(and image
                       (blog-html-tag "meta" '(property og:image) `(content ,image)))
                 ,(blog-html-tag "meta" '(property og:type) `(content ,type))
                 ,(and (equal type "article")
                       (blog-html-tag "meta" '(property article:author) `(content ,author)))
                 ,(and (equal type "article")
                       (blog-html-tag "meta" '(property article:published_time
                                               `(content ,(format-time-string "%FT%T%z" date))))))

I’m using the blog-publish-to-html function as a :publishing-function for html pages, in order to:

  1. prevent hidden articles/pages (which filename start with the prefix ’_’) to be published, mostly using it for unfinished works
  2. adding a subtitle with the publishing date to every post
  3. adding extra meta tags like favicon, rss feed, or open-graph tags to each webpage using the function above.
(defun blog-publish-to-html (plist filename pub-dir)
  "Publish the file to html, checking if FILENAME starts with
 the character `_', representing an hidden post.
 Also adds post date as subtitle and extra head tags for SEO.
 PLIST contains the properties, FILENAME the source file and
 PUB-DIR the output directory."
  (when (not (string-prefix-p "_" filename))
    (let ((project (cons 'rw plist)))
      ;; Adding date subtitle
      (plist-put plist :subtitle (blog-format-date-subtitle filename project))
      ;; Adding extra metatags
      (plist-put plist :html-head-extra (blog-html-head-extra filename project))
      (org-html-publish-to-html plist filename pub-dir))))

Layout files

Layouts are used to replace the default preamble and postamble with a custom header and footer for the page.

The head layout layouts/head.html is used for adding some hmtl code to the page <head> like loading CSS.

<link rel="stylesheet" href="/css/styles.css" type="text/css">
<link rel="alternate" type="application/rss+xml" href="rss.xml" title="RSS feed">

layouts/preamble.html adds the article top menu.

<div class="nav">
      <a href="/">Home</a>

layouts/preamble-header.html is the homepage header with the title.

<div class="header">
  <h1 class="header-title">What the <span class="primary">d</span>ormouse said?</h1>

layouts/postamble.html this is the page footer.

  <div title="GNU Emacs 28.2">
    Made with Emacs
  <div class="links">
    <a href="/about.html">about</a>,
    <a href="/rss.xml">rss</a>,
    <a href="" target="_blank">source</a>

Now, after everything has been configured correctly the only thing it takes to publish/updating the blog is M-x org-publish and choose blog-publish.


I like to see a preview of the blog before uploading it. To do that I’m using an extension called httpd4. It runs a basic http server using the command httpd-server-directory, and I need to specify the blog’s public/ directory to serve. After, opening the browser to localhost:8080 I’m able to see my blog. For the moment, every update needs a page reloading, but maybe in the future I’ll look into adding the hot reloading feature when I publish a new version of the files.