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:
- Create a new
org-mode
buffer withC-x b
and write the post - Save the post in the correct folder
C-x C-s
- Run the build process with
M-x org-publish
and selectedblog-publish
- Push the
public/
folder to the git repo usingmagit
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 processcss/
for the stylesheetsjs/
for the js (now still empty)img/
for the imageslayouts/
for reusable portions of htmlposts/
for theorg
files used as posts, plus thesitemap.org
file- other pages as
index.org
andabout.org
public/
is for the output folder of the build processblog.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)
Project
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 "whatthedormousesaid.co" :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 `(("org" :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 sitemap.org
and then will convert it into HTML when included into index.org
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.
("posts" :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.org" :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.
("img" :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) ("js" :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) ("css" :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) ("fonts" :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.
("rss" :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 '("rss.org" "index.org" "404.org" "sitemap.org")) :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 "rss.org" :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)))) (with-temp-buffer (insert-file-contents (expand-file-name (format "%s.html" filename) layout-dir)) (buffer-string))))
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 rss.org
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 rss.org
.
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 :PROPERTIES: :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 rss.org. DIR is the location of the output." (if (equal "rss.org" (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/sitemap.org
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 "404.org") (string-prefix-p "_" entry)) (format "[[file:%s][%s]] /%s/" entry (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)))) list))) (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 "null.net"'))
will produce a string as <link href="null.net">
.
(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)) (org-export-options-alist `((: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")) project-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)))))) "\n")))
I’m using the blog-publish-to-html
function as a :publishing-function
for html pages, in order to:
- prevent hidden articles/pages (which filename start with the prefix ’_’) to be published, mostly using it for unfinished works
- adding a subtitle with the publishing date to every post
- 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"> <ul> <li> <a href="/">Home</a> </li> </ul> </div>
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> </div>
layouts/postamble.html
this is the page footer.
<footer> <div title="GNU Emacs 28.2"> Made with Emacs </div> <div class="links"> <a href="/about.html">about</a>, <a href="/rss.xml">rss</a>, <a href="https://github.com/elias94/what-the-dormouse-said" target="_blank">source</a> </div> </footer>
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
.
Preview
I like to see a preview of the blog before uploading it. To do that I’m using an extension called httpd
4. 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.
Footnotes:
See here https://orgmode.org/features.html
An updated list of self-closing html5 tags can be found here: http://xahlee.info/js/html5_non-closing_tag.html