UP | HOME
Land of Lisp

Zhao Wei

How can man die better than facing fearful odds, for the ashes of his fathers and the temples of his Gods? -- By Horatius.

Org to blog using org-publish

Table of Contents

1. Introduction

I have tried many org to html or markdown to generate blog posts using different framework. They are all not stable enough. This article describes my configuration step by step to use org-publish to build my blog.

The key point is to use org-publish variable org-publish-project-alist.

2. The structure of configuration

The configuration should meet the following requirements:

  1. Most of the configuration should located inside ~/.emacs.d folder and most of the configuration should be setup using elisp.
  2. Suppose my org files are ready to be exported located at /code/capture-org/publish. After exported (recursively), they should be converted as html files using /code/blog/ as blog base folder.
  3. During exporting, some templates such as headers and footers are added automatically.
  4. Different resources(org files, images, css, other assets, etc) probably are located in different projects or folders. When execute M-x org-publish, those resources will be exported into /code/blog/ with corresponding structure to form our blog.

3. Configurations in detail

3.1. Configuration put in Emacs

(use-package htmlize
  :defer t
  :commands (org-export-dispatch)
  :ensure t)

(use-package ox-reveal
  :ensure t
  :commands (org-export-dispatch)
  :config
  (setq org-reveal-root "https://cdn.jsdelivr.net/npm/reveal.js")
  (setq org-reveal-mathjax t))

(setq blog-org-files-dir "~/code/capture-org/publish/")
(setq emacs-config-base-dir "~/.emacs.d/")
(setq blog-publish-base-dir "~/code/blog/")

;; ref: https://orgmode.org/worg/org-tutorials/org-publish-html-tutorial.html#org625a5c5
(after-load 'org
  (require 'ox-publish)

  (setq org-export-with-section-numbers nil
        org-export-with-smart-quotes t
        org-export-with-toc 3)

  (defvar this-date-format "%b %d, %Y")

  (setq org-html-divs '((preamble "header" "top")
                        (content "main" "content")
                        (postamble "footer" "postamble"))
        org-html-container-element "section"
        org-html-metadata-timestamp-format this-date-format
        org-html-checkbox-type 'html
        org-html-html5-fancy t
        org-html-validation-link t
        org-html-doctype "html5"
        org-html-htmlize-output-type 'css
        org-src-fontify-natively t)


  (defvar me/website-html-head
    "<link rel='icon' type='image/x-icon' href='/images/favicon.ico'/>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel='stylesheet' href='https://code.cdn.mozilla.net/fonts/fira.css'>
<link rel='stylesheet' href='/css/site.css?v=2' type='text/css'/>
<link rel='stylesheet' href='/css/custom.css' type='text/css'/>
<link rel='stylesheet' href='/css/syntax-coloring.css' type='text/css'/>")

  (defun me/website-html-preamble (plist)
    "PLIST: An entry."
    (if (org-export-get-date plist this-date-format)
        (plist-put plist
                   :subtitle (format "Published on %s by %s."
                                     (org-export-get-date plist this-date-format)
                                     (car (plist-get plist :author)))))
    ;; Preamble
    (with-temp-buffer
      (insert-file-contents "~/.emacs.d/html-templates/preamble.html") (buffer-string)))

  (defun me/website-html-postamble (plist)
    "PLIST."
    (concat (format
             (with-temp-buffer
               (insert-file-contents "~/.emacs.d/html-templates/postamble.html") (buffer-string))
             (format-time-string this-date-format (plist-get plist :time)) (plist-get plist :creator))))

  (defvar site-attachments
    (regexp-opt '("jpg" "jpeg" "gif" "png" "svg"
                  "ico" "cur" "css" "js" "woff" "html" "pdf"))
    "File types that are published as static files.")


  (defun me/org-sitemap-format-entry (entry style project)
    "Format posts with author and published data in the index page.

ENTRY: file-name
STYLE:
PROJECT: `posts in this case."
    (cond ((not (directory-name-p entry))
           (format "*[[file:%s][%s]]*
                 #+HTML: <p class='pubdate'>by %s on %s.</p>"
                   entry
                   (org-publish-find-title entry project)
                   (car (org-publish-find-property entry :author project))
                   (format-time-string this-date-format
                                       (org-publish-find-date entry project))))
          ((eq style 'tree) (file-name-nondirectory (directory-file-name entry)))
          (t entry)))


  (defun me/org-reveal-publish-to-html (plist filename pub-dir)
    "Publish an org file to reveal.js HTML Presentation.
FILENAME is the filename of the Org file to be published.  PLIST
is the property list for the given project.  PUB-DIR is the
publishing directory. Returns output file name."
    (let ((org-reveal-root "http://cdn.jsdelivr.net/reveal.js/3.0.0/"))
      (org-publish-org-to 'reveal filename ".html" plist pub-dir)))

  (setq org-publish-project-alist
        `(("posts"
           :base-directory ,blog-org-files-dir
           :base-extension "org"
           :recursive t
           :publishing-function org-html-publish-to-html
           :publishing-directory ,blog-publish-base-dir
           :exclude ,(regexp-opt '("README.org" "draft"))
           :auto-sitemap t
           :sitemap-filename "index.org"
           :sitemap-title "Blog Index"
           :sitemap-format-entry me/org-sitemap-format-entry
           :sitemap-style list
           :sitemap-sort-files anti-chronologically
           :html-link-home "/"
           :html-link-up "../"
           :html-head-include-scripts t
           :html-head-include-default-style nil
           :html-head ,me/website-html-head
           :html-preamble me/website-html-preamble
           :html-postamble me/website-html-postamble)
          ("about"
           :base-directory ,(concat emacs-config-base-dir "about")
           :base-extension "org"
           :exclude ,(regexp-opt '("README.org" "draft"))
           :index-filename "index.org"
           :recursive nil
           :publishing-function org-html-publish-to-html
           :publishing-directory ,(concat blog-publish-base-dir "about")
           :html-link-home "/"
           :html-link-up "/"
           :html-head-include-scripts t
           :html-head-include-default-style nil
           :html-head ,me/website-html-head
           :html-preamble me/website-html-preamble
           :html-postamble me/website-html-postamble)
          ("css"
           :base-directory ,(concat emacs-config-base-dir "css")
           :base-extension "css"
           :publishing-directory ,(concat blog-publish-base-dir "css")
           :publishing-function org-publish-attachment
           :recursive t)
          ("images"
           :base-directory ,(concat blog-org-files-dir "images")
           :base-extension ,site-attachments
           :publishing-directory ,(concat blog-publish-base-dir "images")
           :publishing-function org-publish-attachment
           :recursive t)
          ("common-images"
           :base-directory ,(concat emacs-config-base-dir "images")
           :base-extension ,site-attachments
           :publishing-directory ,(concat blog-publish-base-dir "images")
           :publishing-function org-publish-attachment
           :recursive t)
          ("assets"
           :base-directory ,(concat blog-org-files-dir "assets")
           :base-extension ,site-attachments
           :publishing-directory ,(concat blog-publish-base-dir "assets")
           :publishing-function org-publish-attachment
           :recursive t)
          ("all" :components ("posts" "about" "css" "images" "common-images" "assets")))))

(provide 'init-org-html)

The most important settings are:

  • base-directory, controls from where the org-publish export.
  • publishing-directories, controls to where the org-publish export.
    With carefully arrangement, we could export files from multiple places into one destination.
  • The final “all” entry is the command we will choose when execute M-x org-publish. It will run other entry listed in its components one by one.
    So, it will export for “post”, “about”, “css”, …, etc.

3.2. Templates

There are two kinds of templates. One is used by org file directly as org options. Another is used during export such that the generated html file will include those templates. So, the later one contains html related settings.

3.2.1. HTML templates

  • Define templates

    Since this kind of template will not likely to be changed, I put them into /.emacs.d/html-templates

    • preamble.html

      Land of Lisp

      Zhao Wei

      How can man die better than facing fearful odds, for the ashes of his fathers and the temples of his Gods? -- By Horatius.

      This file controls header.

    • postamble.html

      This fill controls the footer.

  • Use HTML template files

    They are used in init-org-html.el, see init-org-html.el

    (setq org-html-divs '((preamble "header" "top")
                          (content "main" "content")
                          (postamble "footer" "postamble"))
          org-html-container-element "section"
          org-html-metadata-timestamp-format this-date-format
          org-html-checkbox-type 'html
          org-html-html5-fancy t
          org-html-validation-link t
          org-html-doctype "html5"
          org-html-htmlize-output-type 'css
          org-src-fontify-natively t)
    

3.2.2. CSS templates

Our website will use external css file to improve its looking. I put them all under foler /.emacs.d/css. Then, refer them accordingly from org-publish settings defined using elisp shown above. For example, In my custom.css, I define settings related to “table of content” such that it could make the generated “toc” hovering at the top right corner.

/* TOC inspired by http://jashkenas.github.com/coffee-script */
/* https://stackoverflow.com/questions/12232675/how-to-make-table-of-contents-floating */
#table-of-contents {
    font-size: 10pt;
    position: fixed;
    z-index: 1000000;
    background: rgba(255,255,255,.80);
    right: 0em;
    top: 3em;
    color: #dcdccc;
    -webkit-box-shadow: 0 0 1em #777777;
    -moz-box-shadow: 0 0 1em #777777;
    -webkit-border-bottom-left-radius: 5px;
    -moz-border-radius-bottomleft: 5px;
    text-align: right;
    /* ensure doesn't flow off the screen when expanded */
    max-height: 80%;
    overflow: auto;
    line-height: 1em;
}
#table-of-contents h2 {
    font-size: 10pt;
    max-width: 8em;
    font-weight: normal;
    padding-left: 0.5em;
    padding-left: 0.5em;
    padding-top: 0.05em;
    padding-bottom: 0.05em; }
#table-of-contents li {
    margin: 3px 0;
}
#table-of-contents ul {
    padding-left: 2em;
}
#table-of-contents #text-table-of-contents {
    display: none;
    text-align: left; }
#table-of-contents:hover #text-table-of-contents {
    display: block;
    padding: 0.5em;
    margin-top: -1.5em; }

Check previous section about using elisp to configure html stylesheet part.

3.2.3. (Optional) Org template

Here, we setup a template called post.org in which it contains the following options:

#+AUTHOR: Zhao Wei
#+OPTIONS: toc:t
#+HTML_DOCTYPE: xhtml-strict

For a org file, we could use it as other org file options like:

#+title: Sample Post
#+date: <2020-02-05 Wed>
#+filetags: gitlab git org-mode orgmode emacs template sample
#+setupfile: ~/.emacs.d/org-templates/post.org

4. How to use?

  1. Write org files ready to be published at /code/capture-org/publish.
  2. Run M-x org-publish RET, choose all.
  3. Check /code/blog to see html files and other static resources are exported correctly.
  4. Run the following command to start a simple python server to render our static blog website

    # replace directory with your own blog base folder which stores the exported html files from org files
    # for python3.7 above
    python3 -m http.server --directory ~/code/blog/ 3000
    # for python3.6,
    # cd into published content dir 
    python3 -m http.server 3000
    

    If there is no error, visit: http://localhost:3000

  5. Host our website on GitHub pages
    • Following the instructions from GitHub to create an empty repository like <your-username>.github.io.
    • Git initialize our blog and set remote origin to be the repository we just created.
    • After push and wait 3~5 mins, you should be able to visite the blog at: https://zwpdbh.github.io/

5. Improvement

Sometimes, running export from inside Emacs is slow and bloc my current task. So, I need to execute an other emacs process to do org-pubish-all.

  1. First, define our publish.el which utilize the same configuration from my /.emacs.d/:

    (add-to-list 'load-path (expand-file-name "lisp" user-emacs-directory))
    
    (defconst *spell-check-support-enabled* nil) ;; Enable with t if you prefer
    (defconst *is-a-mac* (eq system-type 'darwin))
    
    (require 'init-const)
    (require 'init-utils)     ;; the file provide useful common functions
    (require 'init-site-lisp) ;; Must come before elpa, as it may provide package.el 
    (require 'init-elpa)      ;; Machinery for installing required packages
    (require 'init-exec-path) ;; Set up $PATH
    
    (unless (package-installed-p 'use-package)
      (package-refresh-contents)
      (package-install 'use-package))
    
    ;; load org export related configuration
    (require 'init-org)
    (require 'init-org-babel)
    (require 'init-org-html)
    
    (require 'org)
    (require 'ob)
    (require 'ob-js)
    (require 'org-eldoc)
    (require 'org-tempo)
    (require 'org-table)
    
    (if (member "t" command-line-args)
        (progn
          (print "force publish all org files")
          (org-publish-all t)
          )
        (progn
          (print "only publish modified org files")
          (org-publish-all)))
    
  2. Then, execute emacs to load publish.el

    # By default, only modified files including new files are pubished
    emacs --script "~/.emacs.d/publish.el"
    # We could also force to publish all files
    emacs --script "~/.emacs.d/publish.el" t
    
  3. Last, cd into blog base dir

6. References