Blog with Emacs and Org mode

I will show you the code that makes Emacs become a static site generator with Org mode

My first post explained why I blog with Emacs and Org Mode. This post will show you my configuration for Emacs about how to blog with Org mode. I write the configuration inspired by the post1. So we have many things in commons and also things different.

Org blog post template. Every time to write a new article, I will use org-capture2 to generate a article full of blank, which looks like:

org-capture-template.png

Firstly, create a template showed on the above image and add it to `org-capture-templates' like:

(add-to-list 'org-capture-templates
             `("b" "Blog Post" plain
               (file capture-blog-post-file)
               template))

From post1 I read, (file (capture-blog-post-file)) will signal an error. You should read the docs about `org-capture-templates' by using "C-h v org-capture-template".

Secondly, define a key map for org-capture2 and a function named `capture-blog-post-file'.

  (define-key global-map "\C-cc" 'org-capture)

  (defun capture-blog-post-file ()
    "Return a path where to store post files. This path will be important.
Usually calling `org-capture' will store the captured content into
a existed file. We do something unusual that store the captured content
into a non-existed file.
When calling `org-capture', it will let you input the post file name,
the TITLE and something things accroding to the templates specified by
the `org-capture-templates'. "
    (let* ((title (read-string "Slug: "))
           (slug (replace-regexp-in-string "[^a-z]+" "-" (downcase title))))
      (expand-file-name
       (format (concat posts-path "%s/%s.org")
               (format-time-string "%Y/%m" (current-time))
               slug))))

You need to know that variable `posts-path' is the directory to store posts. Define it later. If done this, you can use "C-c c b" to capture and will see the buffer like the above image.

Configure Org Projects. Use Org mode to publish our project.

  (setq project-path "~/Documents/DarkSalt/")

  (setq posts-path (concat project-path "posts/"))

  (setq tags-path (concat project-path "tags/"))

  (setq publish-path (concat project-path "publish/"))

  (setq files-path (concat project-path "files/"))
  (setq

   org-html-doctype "html5"

   org-html-home/up-format "
<div id=\"org-div-home-and-up\">
  <nav>
    <a href=\"/\"><img src=\"../../../img/logo.png\" alt=\"Logo is on the way\"/></a>
    <ul>
      <li><a accesskey=\"H\" href=\"%s\"> Home </a></li>
      <!--<li><a accesskey=\"a\" href=\"/posts\"> Posts </a></li>-->
      <li><a accesskey=\"T\" href=\"/tags\"> Tags </a></li>
      <li><a accesskey=\"A\" href=\"/about\"> About </a></li>
    </ul>
  </nav>
</div>
"
   org-html-head (concat
                  "<link rel=\"stylesheet\" type=\"text/css\" href=\"../../../css/stylesheet.css\"/>\n"
                  "<link rel=\"icon\" type=\"image/png\" href=\"../../../img/icon.png\" />")

   org-html-scripts "
<script type=\"text/javascript\">
if(/superloopy\.io/.test(window.location.hostname)) {
  (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
  (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
  m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
  })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
  ga('create', 'UA-4113456-6', 'auto');
  ga('send', 'pageview');
}</script>"

   org-html-link-home "/"
   org-html-link-up "/"

   org-export-with-toc nil
   org-export-with-author t
   org-export-with-email nil
   org-export-with-creator nil
   org-export-with-date nil
   org-export-with-section-numbers nil

   org-html-preamble nil
   org-html-postamble t
   org-html-postamble-format `(("en"
                                ,(concat "<p class=\"author\">Author: %a (%e)</p>\n"
                                         "<p class=\"date\">Date: %d</p>\n"
                                         "<p class=\"creator\">Generated by %c</p>")))
   org-publish-project-alist
   `(("static"
      :base-directory ,project-path
      :base-extension "js\\|css\\|png\\|jpg\\|pdf"
      :publishing-directory ,publish-path
      :publishing-function org-publish-attachment
      :exclude "publish"
      :recursive t)
     ("home"
      :base-directory ,project-path
      :base-extension "org"
      :publishing-directory ,publish-path
      :publishing-function org-html-publish-to-html
      :html-head-extra "<link rel=\"stylesheet\" type=\"text/css\" href=\"../../../css/index.css\"/>\n"
      :recursive t
      :exclude "publish")
     ("about"
      :base-directory ,(concat project-path "about/")
      :base-extension "org"
      :publishing-directory ,(concat publish-path "about/")
      :publishing-function org-html-publish-to-html
      :recursive t
      :exclude "publish")
     ("posts"
      :base-directory ,posts-path; ,(concat project-path "posts/")
      :makeindex t
      :publishing-directory ,(concat publish-path "posts/")
      :publishing-function org-html-publish-to-html
      :exclude "publish"
      :recursive t)
     ("tags"
      :base-directory ,tags-path ; ,(concat project-path "tags/")
      :base-extension "org"
      :publishing-directory ,(concat publish-path "tags/")
      :publishing-function org-html-publish-to-html
      :recursive t
      :exclude "publish")
     ("files"
      :base-directory ,files-path
      :base-extension "js\\|css\\|png\\|jpg\\|pdf"
      :publishing-directory ,(concat publish-path "files/")
      :publishing-function org-publish-attachment
      :exclude "publish"
      :recursive t)
     ("DarkSalt" :components ("static" "home" "about" "posts" "tags" "files"))))

The structure of my blog:

;; Project
;;    |- index.org
;;    |- theindex.inc
;;    |- about/
;;    |     `- index.org
;;    |- posts/
;;    |     |- theindex.org
;;    |     |- theindex.inc
;;    |     |- 2018/
;;    |     |     |- 05/
;;    |     |     |    |- hello-world.org
;;    |     |     |    `- other posts
;;    |     |     `- other months
;;    |     `- 20XX/
;;    |
;;    |- publish/, a mirror of Project, is the another project used to publish
;;    |- tags/
;;    |     |- tag1.org
;;    |-    `- tagxxx.org
;;    |- js/
;;    |- img/
;;    |- files/
;;    `- css/

Now, here is a simple static site generator. We can create a template with "C-c c b", fill the template and save the post with "C-c C-c" after editing finished. The final step is to publish with "M-x org-publish RET".

But we need more. Our static site generator is almost same with the one I read from the post1. Yep, this one also has something missing. It misses an auto-generated post list and tags feature. But fortunately, I have fixed the missing.

For auto-generated post list, I put the list into Project/theindex.inc included by Project/index.org. To do that, we need to retrieve the urls and names of post and write them to Project/theindex.inc before calling `org-publish'.

  (defun retrieve-posts (root)
    "Search all the posts in `project-path', return a list of posts paths"
    (when (file-directory-p root)
      (let ((files (directory-files root t "^[^.][^.].*$" 'time-less-p))
            (res nil))
        (dolist (file files res)
          (if (file-directory-p file)
              (setq res (append res (retrieve-posts file)))
            (when (and (string-suffix-p ".org" file)
                       (not (string-suffix-p "theindex.org" file)))
              (setq res (add-to-list 'res file)))))
        (sort res
              #'(lambda (f1 f2)
                  (string<
                   (read-option-from-post f1 "date" (format-time-string "%Y-%m-%d"))
                   (read-option-from-post f2 "date" (format-time-string "%Y-%m-%d"))))))))

  (defun auto-generate-post-list (root)
    "Search the org files in `project-path', and generate a list of
string consisting of url and title of org-file"
    (let ((files (retrieve-posts root))
          res)
      (dolist (file files res)
        (setq res (add-to-list 'res (format "[[file:%s][%s]]"
                                       (replace-regexp-in-string
                                        "\\.org"
                                        ".html"
                                        (file-relative-name file project-path))
                                       (read-option-from-post
                                        file "TITLE" (file-name-base file))))))))


  (defun rewrite-theindex-inc ()
    "Rewrite theindex.inc in `project-path'"
      (write-region
       (mapconcat
        #'(lambda (str) (format "*** %s\n\t" str))
        (auto-generate-post-list posts-path) ; The bug come from this expression
        "\n")
       nil
       (concat project-path "theindex.inc")))

;; define an advising function
 (defadvice org-publish-project
      (before org-publish-project-rewrite-theindex-inc activate)
    (rewrite-theindex-inc))

  (defadvice org-publish-projects
      (before org-publish-projects-rewrite-theindex-inc activate)
    (rewrite-theindex-inc))


The way to provide tags feature is analogous to the way to provide the auto-generated post list. Each tag will has a tag.org including a tag.inc file, an the tag.inc will include url and name of every post tagged with the tag. Confusion, right?

  (defun tag-list (root)
    "Retrieve tags from posts, return a list of tags"
    (let ((files (retrieve-posts root))
          res)
      (dolist (file files res)
        (setq res (append res (retrieve-tags-from-post file))))
      (sort (remove-duplicates res :test 'string=) 'string<)))


  (defun retrieve-tags-from-post (post)
    "Retrieve tags from a post"
    (mapcar
     #'(lambda (elt)
         (--> elt
              downcase
              capitalize))
       (let ((tags (read-option-from-post post "tags")))
         (cond
          ((or (null tags)
               (string= (string-trim tags) "")) (list "Others"))
          (t (split-string (string-trim tags) " "))))))

  (defun posts-of-tag (tag &optional root)
    "Find the posts of tag, return a list of post.
The ROOT points to the directory where posts store on."
    (let ((files (retrieve-posts (or root posts-path)))
          res)
      (dolist (file files res)
        (when (member tag (retrieve-tags-from-post file))
          (setq res (add-to-list 'res file))))
      (cons tag (list (sort res 'string<)))))

  (defun group-posts-by-tags (root)
    "Return a alist of (TAG . (list POST)).
The ROOT points to the directory where posts store on."
    (let ((tags (tag-list root))
          res)
      (dolist (tag tags res)
        (setq res (add-to-list 'res (posts-of-tag tag))))))

  (defun write-posts-to-tag-inc ()
    (let ((grouped-posts (group-posts-by-tags posts-path))
          (tags (tag-list posts-path)))
      (write-region
       (format "#+TITLE: TAGS\n\n%s"
               (mapconcat
                #'(lambda (tag)
                    (format "- [[file:%s][%s]]"
                            (file-relative-name
                             (concat tags-path tag ".html")
                             project-path)
                            tag))
                (tag-list posts-path)
                "\n"))
       nil (concat tags-path "index.org"))
      (dolist (tag tags)
        (write-region
         (mapconcat
          #'(lambda (post)
              (format "- [[file:%s][%s]]"
                      (file-relative-name post tags-path)
                      (read-option-from-post
                       post "TITLE" (file-name-base post))))
          (cadr (assoc tag grouped-posts)) "\n")
         nil (concat tags-path tag ".inc"))
        (unless (file-exists-p (concat tags-path tag ".org"))
          (write-region
           (format "#+TITLE: %s\n#+INCLUDE: %s"
                   tag (concat tag ".inc"))
           nil (concat tags-path tag ".org"))))))

;; modify the advising function
 (defadvice org-publish-project
      (before org-publish-project-rewrite-theindex-inc activate)
    (write-posts-to-tag-inc)
    (rewrite-theindex-inc))

 (defadvice org-publish-projects
      (before org-publish-project-rewrite-theindex-inc activate)
    (write-posts-to-tag-inc)
    (rewrite-theindex-inc))

Now the tag feature is also provided, our static site generator is completed.

That is all. If you have any question then to check my configuration here3 or file me a issue. It is not perfect yet, like not tags generated on post automatically, no way to provide templates for the pages. The will be provided in some days. I will write an other post if the day comes. (It is tough to write a article in English for me, but it is very interesting, I will keep going even though full of mistakes. Hope you can understand what I wrote, lamo.)

Footnotes:

Author: saltb0rn (asche34@outlook.com)

Date: 2018-05-27

Emacs 28.2 (Org mode 9.5.5)

Validate