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:
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:
The blog I read: Blogging with Org mode And using Emacs as a build tool
The docs about org-capture: https://orgmode.org/manual/Capture.html#Capture
My configuration: https://github.com/saltb0rn/emacs.d/tree/v0.1