Minimalist Blogging With Emacs and GitHub Pages
by Philippe Olivier
Created 2021-04-23
1. Introduction
We will discuss a simple and minimalist way of blogging, using Emacs and GitHub Pages. Here, we understand minimalist to be both in the context of a minimal setup and minimal dependencies. The present blog uses this exact setup. What this setup has to offer:
- Create everything in org files, which will then be exported as a HTML website.
- Customize style with CSS.
- A single
M-x
command will publish your org files directly to GitHub Pages. - The option to publish locally.
- Little complexity, little dependencies.
1.1. Dependencies
The dependencies are as follows:
- Software: Emacs, git.
- Emacs packages: magit.
- Other: GitHub Pro account or higher.
1.2. Process
The general process is very simple:
- Create/modify org files (blog posts, etc).
then
- Use a custom elisp command to generate HTML files suitable for local viewing.
or
- Use a custom elisp command to generate and publish HTML files suitable for public viewing (GitHub Pages).
2. Files
Let's take a look at how the files are organized. The following directory layout and file contents is a working example.
2.1. Directory Layout
We will consider that the root directory for the blog on our local machine is ~/blog/
. The two main components of the layout are org/
, which is where we will create and modify files, and html/
, where the generated HTML files will be exported to.
~/blog/ |- org/ | |- about.org | |- css/ | | `- style.css | |- img/ | |- index.org | `- posts/ | `- first-post.org `- html/
org/about.org
: In this example we have an "about me" page.org/css/style.css
: The CSS stylesheet.org/img/
: The image folder.org/index.org
: The main page of the blog.org/posts/
: A directory containing the various blog posts, withfirst-post.org
as an example of a blog post.html/
: The directory where the HTML files will be generated/exported.
2.2. about.org
The contents of about.org
are:
#+options: ':nil *:t -:t ::t <:nil H:3 \n:t ^:t arch:headline #+options: author:nil broken-links:mark c:nil creator:nil #+options: d:(not "LOGBOOK") date:nil e:nil email:nil f:t inline:t num:t #+options: p:nil pri:nil prop:nil stat:nil tags:nil tasks:nil tex:t #+options: timestamp:nil title:t toc:nil todo:nil |:t #+title: About #+author: My Name #+language: en I'll tell you about me folks, because I'm a very interesting person. Bill Gates is not as interesting as me. I'm much more interesting. They say I'm the most interesting person they've ever seen. It's true!
The top portion of the file is the export options. You can insert an export template with C-c C-e # default RET
, then modify the options as you see fit. Documentation for these options can be found here.
2.3. style.css
The contents of style.css
are:
body { background-color:#FFFFFF; color:#000000; font-family:serif; margin-left:10%; margin-right:10%; margin-bottom:50px; } .preamble { background-color:#FFFFFF; color:#000000; text-align:center; } .postamble { background-color:#FFFFFF; color:#000000; text-align:center; margin-top:50px; font-size:small; } code { background-color:#000000; color:#F5DEB3; font-family:Source Code Pro,monospace; } pre.src { background-color:#000000; color:#F5DEB3; border-color:#3A6EA5; border-width:thin thin thin 10px; font-family:Source Code Pro,monospace; }
The style for most of the page is found in body
. The top and bottom of the page can have a different style, specified in .preamble
and .postamble
. Inline code like this
is defined in code
. The style of source blocks is defined in pre.src
.
2.4. index.org
This is the main page of the blog:
#+options: ':nil *:t -:t ::t <:nil H:3 \n:t ^:t arch:headline #+options: author:nil broken-links:mark c:nil creator:nil #+options: d:(not "LOGBOOK") date:nil e:nil email:nil f:t inline:t num:t #+options: p:nil pri:nil prop:nil stat:nil tags:nil tasks:nil tex:t #+options: timestamp:nil title:t toc:nil todo:nil |:t #+title: My Blog #+author: My Name #+language: en - My First Blog Post!
2.5. first-post.org
Example of a blog post:
#+options: ':nil *:t -:t ::t <:nil H:3 \n:t ^:t arch:headline #+options: author:nil broken-links:mark c:nil creator:nil #+options: d:(not "LOGBOOK") date:nil e:nil email:nil f:t inline:t num:t #+options: p:nil pri:nil prop:nil stat:nil tags:nil tasks:nil tex:t #+options: timestamp:nil title:t toc:nil todo:nil |:t #+title: First Post #+author: My Name #+language: en This is my first blog post! This blog will be the best blog ever!
3. Elisp Stuff
The only thing that remains is the two elisp functions to publish the blog either locally or publicly. You might want to publish locally first, to see what the final result will look like. The HTML links between files are also handled differently locally and publicly.
The preambles and postambles for local and public publishing will be slightly different (w.r.t. the links), so we define them separately:
;; Head, preamble, and postamble, for local paths. (setq me/blog-html-head-local "<link rel='stylesheet' type='text/css' href='~/blog/html/css/style.css'/>") (setq me/blog-html-preamble-local "<div class='preamble'> <a href='~/blog/html/index.html'>Blog</a> | <a href='~/blog/html/about.html'>About</a> </div>") (setq me/blog-html-postamble-local "<div class='postamble'> This website is entirely generated by Emacs. </div>") ;; Head, preamble, and postamble, for public paths. (setq me/blog-html-head-public "<link rel='stylesheet' type='text/css' href='/css/style.css'/>") (setq me/blog-html-preamble-public "<div class='preamble'> <a href='/index.html'>Blog</a> | <a href='/about.html'>About</a> </div>") (setq me/blog-html-postamble-public "<div class='postamble'> This website is entirely generated by Emacs. </div>")
The variable org-publish-project-alist
defines many options for publishing. Since the options will be different depending on if we publish locally or publicly, we wrap the variable in a function, which allows us to change some fields as needed:
(require 'ox-publish) (defun me/org-publish-project-alist-wrapper (html-head html-preamble html-postamble) "Wrapper for the variable `org-publish-project-alist', allowing dynamic modifications of some fields." (setq org-publish-project-alist `(("blog-org" :base-directory "~/blog/org/" :base-extension "org" :publishing-directory "~/blog/html/" :recursive t :publishing-function org-html-publish-to-html :headline-levels 4 :auto-preamble t :html-validation-link nil :html-head ,html-head :html-preamble ,html-preamble :html-postamble ,html-postamble ) ("blog-static" :base-directory "~/blog/org/" :base-extension "css\\|png\\|jpg\\|gif\\|pdf" :publishing-directory "~/blog/html/" :recursive t :publishing-function org-publish-attachment ) ("blog-all" :components ("blog-org" "blog-static")))))
Finally, the function to publish locally:
(defun me/blog-publish-local () "Publish the blog so that it looks normal, locally." (interactive) (me/org-publish-project-alist-wrapper me/blog-html-head-local me/blog-html-preamble-local me/blog-html-postamble-local) (org-publish-project "blog-all" t nil) (message "Blog successfully published (local)."))
The function to publish publicly needs git to be able to push without asking for credentials. One option for this is git-credential-store
. Your GitHub repository is in ~blog/html/
.
(require 'magit) (defun me/blog-publish-public () "Publish the blog so that it looks normal, online." (interactive) (when (y-or-n-p "Publish blog online? ") (me/org-publish-project-alist-wrapper me/blog-html-head-public me/blog-html-preamble-public me/blog-html-postamble-public) (org-publish-project "blog-all" t nil) (progn ;; Select the correct repository. (magit-status "~/blog/html") (magit-call-git "add" "--all") ;; Commit with an auto-generated message. (let* ((commit-message (concat "Commit on " (format-time-string "%F %H:%M" (current-time))))) (magit-call-git "commit" "-m" commit-message)) ;; Push to GitHub. (magit-call-git "push") ;; Kill the magit buffers created by the various operations. (kill-buffer "magit: html") (kill-buffer "magit-process: html") (delete-window)) (message "Blog successfully published (public).")))
Both functions generate the HTML files directly to ~/blog/html/
, and publishing publicly further pushes the changes to the GitHub repository, and by extension to GitHub Pages.