Org-mode to GitHub pages with Jekyll

1 Why org-mode

Org mode is considered to be one of the most remarkable modes in Emacs. Basically, it is a lightweight markup language, which is similar as Markdown and Textile, but is far more powerful. The .org files can be exported to many other formats, such as .tex and .html. We will take advantage of the publishing system provided by org-mode to manage the pages of the website.

2 How to setup org-mode with Jekyll

Here I assume you have already had some basic knowledge of org-mode, Git and Jekyll, and already installed Git and Jekyll on your system. This post is based on these two articles: Using org to Blog with Jekyll and Blogging with Emacs org-mode and Github Pages.

2.1 Basic settings for publishing of org-mode

The configuration of the publishing system in org-mode is done by setting the value of org-publish-project-alist variable.

(setq org-publish-project-alist
      '(("jekyll-cute-jumper-github-io" ;; settings for cute-jumper.github.io
         :base-directory "~/Documents/org-publish/cute-jumper.github.io/org"
         :base-extension "org"
         :publishing-directory "~/Documents/org-publish/cute-jumper.github.io/deploy"
         :recursive t
         :publishing-function org-html-publish-to-html
         :with-toc nil
         :headline-levels 4
         :auto-preamble nil
         :auto-sitemap nil
         :html-extension "html"
         :body-only t)))

Note that the :body-only t is essential because here we only need to generate the content between <body> and </body>, and let Jekyll take care of the rest.

2.2 Jekyll-Bootstrap

For convenience, I start my configuration from Jekyll-Bootstrap.

cd ~/Documents/org-publish/cute-jumper.github.io/
# Clone into publishing directory
git clone https://github.com/plusjade/jekyll-bootstrap.git deploy
cd deploy
# Setup upstream to your remote repo in GitHub
git remote set-url origin git@github.com: cute-jumper/cute-jumper.github.io.git

Then, edit the _config.yml file under the root directory. Check the documentation for details.

2.3 Directory Structure

Here is my directory structure:

/
├── deploy
│  ├── ...other files already in repo
│  ├── about.html
│  ├── _drafts
│  │     └── .html files
│  ├── index.html
│  ├── _posts
│  │     └── .html files
│  └── timeline.html
└── org
    ├── about.org
    ├── _drafts
    │     └── .org files
    ├── index.org
    ├── _posts
    │     └── .org files
    └── timeline.org

Note that there is a mapping from the .org files in org/ to .html files in deploy/. We only need to focus on the org/ directory and write our posts there, and use org-publish to convert these .org files to .html files and run jekyll serve under deploy/, then we can see the results in http://localhost:4000. All the other configurations, such as changing themes and setting up comments should be done in the deploy/ directory, and that's what we use Jekyll for.

2.4 Cooperate with Jekyll

In order to make the exported .html files correctly be processed by Jekyll, we need to make some changes to our .org files.

  • Front-matter. Usually, I put the following snippet at the beginning of every post. This is called front-matter, which will be handled by Jekyll and is required if we want the exported .html understood by Jekyll.

    #+BEGIN_EXPORT html
    ---
    layout: post
    title: Org-mode to GitHub pages with Jekyll
    excerpt: Introduce how to use Emacs's Org-mode with Jekyll to generate GitHub Pages
    categories:
      - Emacs
    tags:
      - [Emacs, org-mode, GitHub, Jekyll]
    ---
    #+END_EXPORT
    
  • Name Convention. By the Jekyll convention, we need to puts our posts(org files) under _posts/, and the file name should be the format yyyy-mm-dd-name.org. The following code, from Blogging with Emacs Org-mode and Jekyll, can simplify our work. I made some slight modifications to the original code to fit my own needs.

    (defvar jekyll-directory (expand-file-name "~/Documents/org-publish/cute-jumper.github.io/org/")
      "Path to Jekyll blog.")
    (defvar jekyll-drafts-dir "_drafts/"
      "Relative path to drafts directory.")
    (defvar jekyll-posts-dir "_posts/"
      "Relative path to posts directory.")
    (defvar jekyll-post-ext ".org"
      "File extension of Jekyll posts.")
    (defvar jekyll-post-template
      "BEGIN_EXPORT\n---\nlayout: post\ntitle: %s\nexcerpt: \ncategories:\n  -  \ntags:\n  -  \n---\n#+END_EXPORT\n\n* "
      "Default template for Jekyll posts. %s will be replace by the post title.")
    
    (defun jekyll-make-slug (s)
      "Turn a string into a slug."
      (replace-regexp-in-string
       " " "-" (downcase
                (replace-regexp-in-string
                 "[^A-Za-z0-9 ]" "" s))))
    
    (defun jekyll-yaml-escape (s)
      "Escape a string for YAML."
      (if (or (string-match ":" s)
              (string-match "\"" s))
          (concat "\"" (replace-regexp-in-string "\"" "\\\\\"" s) "\"")
        s))
    
    (defun jekyll-draft-post (title)
      "Create a new Jekyll blog post."
      (interactive "sPost Title: ")
      (let ((draft-file (concat jekyll-directory jekyll-drafts-dir
                                (jekyll-make-slug title)
                                jekyll-post-ext)))
        (if (file-exists-p draft-file)
            (find-file draft-file)
          (find-file draft-file)
          (insert (format jekyll-post-template (jekyll-yaml-escape title))))))
    
    (defun jekyll-publish-post ()
      "Move a draft post to the posts directory, and rename it so that it
     contains the date."
      (interactive)
      (cond
       ((not (equal
              (file-name-directory (buffer-file-name (current-buffer)))
              (concat jekyll-directory jekyll-drafts-dir)))
        (message "This is not a draft post."))
       ((buffer-modified-p)
        (message "Can't publish post; buffer has modifications."))
       (t
        (let ((filename
               (concat jekyll-directory jekyll-posts-dir
                       (format-time-string "%Y-%m-%d-")
                       (file-name-nondirectory
                        (buffer-file-name (current-buffer)))))
              (old-point (point)))
          (rename-file (buffer-file-name (current-buffer))
                       filename)
          (kill-buffer nil)
          (find-file filename)
          (set-window-point (selected-window) old-point)))))
    
  • Link Syntax. Jekyll supports many kinds of links. For example, by default this post, named 2013-10-06-orgmode-to-github-pages-with-jekyll.org, will finally have a link /emacs/2013/10/06/orgmode-to-github-pages-with-jekyll/, which means the original org-mode's link syntax [[file:/path/to/file][description]] will not work because the final URL will change unless you modify the settings to prohibit Jekyll from doing such a path transformation. Thanks to the high customizability of org-mode, there is an elegant way to solve this. I stole from this thread in stackoverflow.

    (defun org-jekyll-post-link-follow (path)
      (org-open-file-with-emacs path))
    
    (defun org-jekyll-post-link-export (path desc format)
      (cond
       ((eq format 'html)
        (format "<a href=\"{%% post_url %s %%}\">%s</a>" path desc))))
    
    (org-add-link-type "jekyll-post" 'org-jekyll-post-link-follow 'org-jekyll-post-link-export)
    

    When we want to link to a post, we now use the syntax: [[jekyll-post:filename][description]] instead.

2.5 Work with Templates

In order to make use of Jekyll's template system, we sometimes need to embed the template code into our org files. Thus, we may need #+BEGIN_EXPORT html...#+END_EXPORT structure very often.

Following is the current version of my index.org, most of which is copied from Using org to Blog with Jekyll.

#+BEGIN_EXPORT html
---
layout: page
title: cd /home/cute-jumper/
tagline: <br/>$ ls -t blogs.happy.programming/
---
#+END_EXPORT
#+OPTIONS: num:nil
Hello, welcome to the homepage of cute-jumper.

* Posts
#+BEGIN_EXPORT html

<ul class="posts">
  {% for post in site.posts %}
    <li><span>{{ post.date | date_to_string }}</span> &raquo; <a href="{{ BASE_PATH }}{{ post.url }}">{{ post.title }}</a></li>
    <em>{{ post.excerpt}}</em>
  {% endfor %}
</ul>

#+END_EXPORT
* Timeline
#+BEGIN_EXPORT html
<ul class="timeline">
    <li><a href="timeline.html">Timeline</a></li>
</ul>
#+END_EXPORT
* About
#+BEGIN_EXPORT html
<ul class="about">
  <li><a href="about.html">About</a></li>
</ul>
#+END_EXPORT

To be honest, it is kind of wired to embed so many HTML in org file, but it seems inevitable if we want to use the template system.

3 Workflow to Publish to GitHub

Now let us check the whole workflow to publish our web pages after we have done all the settings above.

  1. Invoke jekyll-draft-post to write a draft. (You may want to add deploy/_drafts directory into your .gitignore file…That's what I did.)
  2. When writing is done, invoke jekyll-publish-post, which will prepend a timestamp in front of the original file name and move the file from _drafts/ to _posts/.
  3. Invoke org-publish, and choose jekyll-cute-jumper-github-io.
  4. Enter deploy/ directory, and run jekyll serve to see the results locally.
  5. Commit and push to GitHub using Git.
  6. Done!

It's so easy!

4 Future work

I'm thinking about making my own themes because the official themes in http://themes.jekyllbootstrap.com/ don't fit my needs very well. Maybe someday I will also remove Jekyll-Bootstrap and build my own things from scratch.

Currently this site is just a demo, and much more work needs to be done. I would try to improve this site when I have time.^_^

Junpeng Qiu 06 October 2013
blog comments powered by Disqus