Publishing my Website with Org

Table of Contents

A literate programming file for publishing my website using org.

Introduction

While the Emacs community have a plethora of options for generating a static website from org-formatted files, I keep my pretty simple, and use the standard org-publish feature.

While the following packages come with Emacs, they aren't necessarily loaded:

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

Variable settings:

(setq org-publish-project-alist nil ; filled in below
      org-export-with-broken-links t
      org-mode-websrc-directory "~/website"
      org-mode-publishing-directory (concat (getenv "HOME") "/website-pub/"))

Since I have two specific websites, I create two variables for those destinations:

(setq ha-publishing-howardabrams (concat org-mode-publishing-directory "howardabrams")
      ha-publishing-howardism (concat org-mode-publishing-directory "howardisms"))

You Don’t Know Jack

I’m not afraid of HTML, but I like the idea of doing my HTML work in a Lisp-like way using the jack-html project:

(use-package jack)

So the Lisp code:

(jack-html '(:p "Hello there"))

Returns the string:

<p>Hello there</p>

The Projects

I separate my website into distinct projects separately built:

blog-content
The bulk of rendering my website org files into HTML
blog-static
Copies all assets, like images, in place
blog-rss
Regenerate the feeder files
org-notes
Optionally render a non-web site collection of notes.

The Website

Years of having two domain names can be confusing, but I have to keep them going now. My howardabrams.com is essentially a single static page (oh, and email).

(add-to-list 'org-publish-project-alist
             `("website"
               :base-directory       "~/dropbox/website-howardabrams"
               :publishing-directory ,ha-publishing-howardabrams))

The Blog

My main blog made up a collection of org files:

(add-to-list 'org-publish-project-alist
             `("blog-content"
               :base-directory       ,org-mode-websrc-directory
               :base-extension       "org"
               :publishing-directory ,org-mode-publishing-directory
               :recursive            t
               :publishing-function  org-html-publish-to-html
               :preparation-function org-mode-blog-prepare
               :export-with-tags     nil
               :headline-levels      4
               :auto-preamble        nil
               :auto-postamble       nil
               :auto-sitemap         t
               :sitemap-title        "Howardisms"
               :section-numbers      nil
               :table-of-contents    nil
               :with-toc             nil
               :with-author          nil
               :with-creator         nil
               :with-tags            nil
               :with-smart-quotes    t

               :html-doctype         "html5"
               :html-html5-fancy     t
               :html-head-include-default-style nil
               ;; :html-preamble  org-mode-blog-preamble
               :html-postamble org-mode-blog-postamble
               ;; :html-postamble "<hr><div id='comments'></div>"
               :html-head-extra
               ,(jack-html
                 '((:link (@ :rel "stylesheet"
                      :type "text/css"
                      :href "http://fonts.googleapis.com/css?family=Source+Sans+Pro:400,700&subset=latin,latin-ext"))
                   (:link (@ :rel "stylesheet"
                      :type "text/css"
                      :href "http://fonts.googleapis.com/css?family=Source+Serif+Pro:400,700&subset=latin,latin-ext"))
                   (:link (@ :rel "stylesheet"
                      :type "text/css"
                      :href "http://fonts.googleapis.com/css?family=Source+Code+Pro:400,700"))
                   (:link (@ :rel "stylesheet"
                      :type "text/css"
                      :href "/css/styles.css"))
                   (:script (@ :type "text/javascript"
                       :src "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"))
                   (:script (@ :src "/js/magic.js" :type "text/javascript"))
                   (:meta (@ :name "viewport" :content "width=device-width, initial-scale=1"))
                   (:link (@ :rel "shortcut icon" :href "/img/dragon-head.svg"))
                   (:link (@ :rel "icon" :href "/img/dragon.svg"))
                   (:link (@ :rel "me" :href "https://emacs.ch/@howard"))
                   (:meta (@ :http-equiv "X-Clacks-Overhead" :content "GNU Terry Pratchett"))))))

Why not break out the images and other static files into a separate project:

(add-to-list 'org-publish-project-alist
             `("blog-static"
               :base-directory       ,org-mode-websrc-directory
               :base-extension       "css\\|js\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|ogg\\|swf\\|svg"
               :publishing-directory ,org-mode-publishing-directory
               :recursive            t
               :publishing-function  org-publish-attachment))

The RSS generation seems to be something I do later once I have my site working:

(add-to-list 'org-publish-project-alist
 `("blog-rss"
         :base-directory        ,org-mode-websrc-directory
         :base-extension        "org"
         :rss-image-url         "https://howardism.org/img/dragon-head.png"
         :publishing-directory  ,org-mode-publishing-directory
         :publishing-function   (org-rss-publish-to-rss)
         :html-link-home        "https://www.howardism.org/"
         :html-link-use-abs-url t
         :with-toc              nil
         :exclude               ".*"
         :include               ("index.org")))

And let’s make some blends of the individual projects:

(add-to-list 'org-publish-project-alist
 `("blog" :components ("blog-content" "blog-static" "blog-rss")))

Technical Notes

I take notes on a variety of technical subjects, and since I can share these notes with others, I feel like I can publish those:

(add-to-list 'org-publish-project-alist
 `("tech-notes"
         :base-directory        "~/technical/"
         :base-extension       "org"
         :publishing-directory ,(concat org-mode-publishing-directory "notes/")
         :recursive            t
         :publishing-function  org-html-publish-to-html
         :headline-levels      4
         :auto-preamble        t
         :auto-sitemap         t  ; Generate sitemap.org automagically...
         :makeindex            t
         :section-numbers      nil
         :style                ,(jack-html
                                 '(:link (@ :rel "stylesheet"
                                            :type "text/css"
                                            :href "../css/styles.css")
                                   :script (@ :type "text/javascript"
                                              :src "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js")
                                   :link (@ :ref "stylesheet"
                                            :type "text/css"
                                            :href "http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/themes/smoothness/jquery-ui.css")
                                   :script (@ :type "text/javascript"
                                              :src "https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/jquery-ui.min.js")
                                   :script (@ :type "text/javascript"
                                              :src "js/magic.js")))
         :table-of-contents    nil
         :with-author          nil
         :with-creator         nil
         :with-tags            nil))

As above, we can separate the publishing of the images and other static files:

(add-to-list 'org-publish-project-alist
 `("tech-notes-static"
         :base-directory       "~/technical/"
         :base-extension       "css\\|js\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|ogg\\|swf"
         :publishing-directory ,(concat org-mode-publishing-directory "/other/")
         :recursive            t
         :publishing-function  org-publish-attachment))

Literate Emacs Configuration

I’ve been committing my literate-style Emacs configuration for years now, and Github has rendered it well, but I felt I could publish this to my own web site as a cleaner version.

(add-to-list 'org-publish-project-alist
   `("hamacs"
     :base-directory        "~/other/hamacs"
     :publishing-directory ,(concat org-mode-publishing-directory "hamacs/")
     :publishing-function  org-html-publish-to-html
     :recursive            t
     :auto-preamble        nil
     :auto-sitemap         nil
     :makeindex            nil
     :section-numbers      nil
     :html-head-include-default-style nil
     :html-head           ,(jack-html
                              '(:link (@ :rel "stylesheet" :type "text/css"
                                :href "../css/styles.css")))
     :html-head-extra      nil
     :table-of-contents    t
     :with-author          nil
     :with-creator         nil
     :with-tags            nil))

(add-to-list 'org-publish-project-alist
   `("hamacs-static"
     :base-directory       "~/other/hamacs"
     :base-extension       "css\\|js\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|ogg\\|swf"
     :publishing-directory ,(concat org-mode-publishing-directory "hamacs")
     :recursive            t
     :publishing-function  org-publish-attachment))

Airbnb

I have an ADU on my property that I rent out through Airbnb. The place is full of QR Codes that display everything from local restaurants to how to play the Raspberry Pi Arcade my son and I built.

(add-to-list 'org-publish-project-alist
   `("airbnb"
     :base-directory        "~/website/airbnb"
     :publishing-directory ,(concat org-mode-publishing-directory "airbnb/")
     :publishing-function  org-html-publish-to-html
     :recursive            t
     :auto-preamble        nil
     :auto-sitemap         nil
     :makeindex            nil
     :section-numbers      nil
     :html-head-include-default-style nil
     :html-head           ,(jack-html
                              '(:link (@ :rel "stylesheet" :type "text/css"
                                :href "airbnb.css")))
     :html-head-extra      nil
     :table-of-contents    nil
     :with-author          nil
     :with-creator         nil
     :with-tags            nil))

(add-to-list 'org-publish-project-alist
   `("airbnb-static"
     :base-directory       "~/website/airbnb"
     :base-extension       "css\\|js\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|ogg\\|swf"
     :publishing-directory ,(concat org-mode-publishing-directory "airbnb/")
     :recursive            t
     :publishing-function  org-publish-attachment))

Including Sections

In the project definitions, I reference a pre- and postamble that allow me to inject some standard HTML file headers and footers:

(defun org-mode-blog-preamble (options)
  "The function that creates the preamble top section for the blog.
    OPTIONS contains the property list from the org-mode export."
  (message "Preamble options: %s" (princ options))
  (let ((base-directory (plist-get options :base-directory)))
    (org-babel-with-temp-filebuffer (expand-file-name "top-bar.html" base-directory) (buffer-string))))

(defun org-mode-blog-postamble (options)
  "The function that creates the postamble, or bottom section for the blog.
  OPTIONS contains the property list from the org-mode export."
  (let ((base-directory (plist-get options :base-directory)))
    (org-babel-with-temp-filebuffer (expand-file-name "bottom.html" base-directory) (buffer-string))))

Another helper function for the content of website is to make sure to update index.org, so that the RSS gets generated.

(defun org-mode-blog-prepare (&optional options)
  "Change modification of `index.org' before publishing."
  (let* ((base-directory (plist-get options :base-directory))
         (buffer (find-file-noselect (expand-file-name "index.org" base-directory) t)))
    (with-current-buffer buffer
      (set-buffer-modified-p t)
      (save-buffer 0))
    (kill-buffer buffer)))

Uploading

Using rsync to keep published files in sync with my website:

(defun ha-sync-site (project)
  "Sync PROJECT (an org publish project) with my website."
  (interactive (list (completing-read "Publish project: "
                                      org-publish-project-alist)))
  (let* ((host "gremlin.howardabrams.com")
         (conf (thread-last org-publish-project-alist
                            (seq-filter (lambda (lst) (string-equal (car lst) project)))
                            (car)
                            (cdr)))
         (parent (plist-get conf :publishing-directory))
         (combos (cond
                ((equal project "blog-content")
                 '("Technical" "howardism"
                   "Personal" "howardism"
                   "index.html" "howardism"
                   "about-me.html" "howardabrams"))
                ((equal project "blog-static") "howardism")
                ((equal project "blog-rss") "howardism")
                ((equal project "tech-notes") "howardabrams/technical")
                ((equal project "tech-notes-static") "howardabrams/technical")
                ((equal project "hamacs") "howardabrams/hamacs")
                ((equal project "hamacs-static") "howardabrams/hamacs")
                ((equal project "airbnb") "howardabrams/airbnb")
                ((equal project "airbnb-static") "howardabrams/airbnb"))))
    ;; (dolist (tuple (seq-partition combos 2))
    ;;   (seq-let (src dest) tuple
    ;;     (format "rsync -az %s/%s %s:%s" parent src host dest)))
    (thread-last (seq-partition combos 2)
                 (seq-map (lambda (tuple) (seq-let (src dest) tuple
                                       (format "rsync -avz %s%s %s:%s" parent src host dest))))
                 (s-join "; ")
                 (async-shell-command))))

Keybindings

Make it easy to publish all projects or single project:

(with-eval-after-load 'ha-org
  (ha-leader :keymaps 'org-mode-map
    "o p"  '(:ignore t :which-key "publish")
    "o p a" '("all" . org-publish-all)
    "o p p" '("project" . org-publish-project)
    "o p s" '("sync site" . ha-sync-site)
    "o p h" '("hamacs" . (lambda () (interactive)
                           (org-publish-project "hamacs")
                           (sit-for 30)
                           (ha-sync-site "hamacs")))))

And let's put a leader key sequence for my favorite file on my website:

(ha-leader
  "f h"  '(:ignore t :which-key "howards")
  "f h i" '("website index" . (lambda ()
                                (find-file (expand-file-name "index.org" "~/website")))))