Daniel Liden

Blog / About Me / Photos / LLM Fine Tuning / Notes /

Made with Org-Mode


I finally made a personal site using org-mode's built-in ox-publish exporter.

I've written my personal website with org-mode for years (it is, after all, one of the most reasonable markup languages to use for text). But until this point, I've used Hugo (with the ox-Hugo exporter). It worked fine, but it always seemed just a little bit too complicated for my needs. I wanted to find something where I could basically understand all of the components and where the gap between my org-mode files and the published output was as small as possible. I wanted to focus more on the writing and less on understanding the framework.

First Steps


This great guide from System Crafters got me started with ox-publish. The very short post how to use ox-publish to set up a basic site; htmlize to correctly render code blocks, and simple-httpd to preview the site locally. I recommend starting here if you're interested in making your own site with org-mode.

GitHub Pages

The followup post is just as useful—it describes the process of publishing the site with GitHub Pages (or SourceHut; I opted for GitHub Pages). There was one minor point missing from this article. The described GitHub action runs the site's build script and commits the public directory to a new branch. It was, therefore, necessary to add the local version of the public branch (which we preview with simple-httpd) to our gitignore file to avoid conflicts. Otherwise, this guide was simple to follow and worked perfectly.

Some Simple Customization

There were a few simple design and organization changes I wanted before I started writing: a simple and readable theme; a list of recent posts with short "preview" snippets, and a link to a post archive.

CSS theme

I opted to use orgcss, at least for now. It's a stylesheet explicitly designed for use with org-exported HTML files. It looks (to me) quite a bit nicer than the default and it works well without any additional configuration. I'm sure I'll want to make some changes in the future, but it's a great place to start.

I applied this stylesheet to all of the exported .org files by adding the following line to my build-site.el configuration file:

;; org-site/build-site.el
;; ...
(setq org-html-head "<link rel=\"stylesheet\" type=\"text/css\" href=\"https://gongzhitaao.org/orgcss/org.css\"/>")

Recent Posts

I wanted to include a few recent posts on the homepage and separately link to the post archive. I also wanted each of these custom posts to have a short "preview"—a paragraph or so of my choosing from the post. I adapted my approach to this from this post (not sure of the author) and this post by Seth Morabito (@twylo).

To include the recent posts, I included the first 25 lines of the automatically-generated sitemap.org on my index page with:

#+INCLUDE: sitemap.org::*posts :lines "-25" :only-contents t

Sitemap Configuration

I specified the generation of the sitemap using org-publish-project-alist configuration variable:

 1: ;; org-site/build-site.el
 2: (setq org-publish-project-alist
 3:       (list
 4:        (list "org-site:main"
 5:              ;; other configuration settings
 6:              :base-directly: "./content"
 7:              :auto-sitemap t
 8:              :sitemap-title nil
 9:              :sitemap-format-entry 'my/org-publish-org-sitemap-format
10:              :sitemap-function 'my/org-publish-org-sitemap
11:              :sitemap-sort-files 'anti-chronologically
12:              :sitemap-filename "sitemap.org"
13:              :sitemap-style 'tree)))

Sitemap Entry Formatting

You'll notice two functions used to format and publish the sitemap entries. sitemap-format-entry takes three arguments (entry, style, and project). We only need to worry about the former. Each entry is a file or directory in the base-directory specified in the org-publish-project-alist above. My my/org-publish-org-sitemap-format defun closely follows the one here.

 1: ;; org-site/build-site.el
 2: (defun my/org-publish-org-sitemap-format (entry style project)
 3:   "Custom sitemap entry formatting: add date"
 4:   (cond ((not (directory-name-p entry))
 5:          (let ((preview (if (my/get-preview (concat "content/" entry))
 6:                             (my/get-preview (concat "content/" entry))
 7:                           "(No preview)")))
 8:          (format "[[file:%s][(%s) %s]]\n%s"
 9:                  entry
10:                  (format-time-string "%Y-%m-%d"
11:                                      (org-publish-find-date entry project))
12:                  (org-publish-find-title entry project)
13:                  preview)))
14:         ((eq style 'tree)
15:          (file-name-nondirectory (directory-file-name entry)))
16:         (t entry)))

This does the following:

  1. Read in each entry's path
  2. If the entry is not a directory:
    • Check if the my/get-preview defun returns a value (more on my/get-preview later)
    • Assign the preview text to to the preview variable if my/get-preview is not null and assigns "(No preview)" to preview otherwise
    • Format the output as a link to the entry with (Date) Title as the description (using the org-publish-find-date and org-publish-find-title defuns to get the dates/titles of each entry)
    • include the preview (defined above) after a line break
  3. If the entry is a directory (e.g. if the first condition returns nil) and if the sitemap-style is tree, return the name of the last subdirectory (e.g. the entry /projects/org-site/content/posts would return posts)
  4. Otherwise, just return the entry unchanged.

Sitemap List Formatting

The entries formatted above are all added to a list (formatted in a tree style to represent the directory structure). We simply convert this list into an org subtree and publish it to the sitemap.org file. The conversion is handled in the my/org-publish-org-sitemap file (again, adapted from here).

1: ;; org-site/build-site.el
2: (defun my/org-publish-org-sitemap (title list)
3:   "Sitemap generation function."
4:   (concat "#+OPTIONS: toc:nil")
5:   (org-list-to-subtree list))

All this does is specify that we do not want a table of contents and that we want our formatted list of entries (with previews) represented as an org subtree.


One part we haven't addressed yet is the generation of previews. There are different approaches out there, but none of them did exactly what I wanted. I borrowed from posts by Seth Morabito and especially Dennis Ogbe. The biggest change I wanted was making sure a default "No preview" would be inserted if there wasn't a preview. I did this by ensuring the my/get-preview defun would return nil (instead of an error) without a preview, and that the entry formatting defun would return "No preview" if my/get-preview was nil.

 1: ;; org-site/build-site.el
 2: (defun my/get-preview (file)
 3:   "get preview text from a file
 5: Uses the function here as a starting point:
 6: https://ogbe.net/blog/blogging_with_org.html"
 7:   (with-temp-buffer
 8:     (insert-file-contents file)
 9:     (goto-char (point-min))
10:     (when (re-search-forward "^#\\+BEGIN_PREVIEW$" nil 1)
11:       (goto-char (point-min))
12:       (let ((beg (+ 1 (re-search-forward "^#\\+BEGIN_PREVIEW$" nil 1)))
13:             (end (progn (re-search-forward "^#\\+END_PREVIEW$" nil 1)
14:                         (match-beginning 0))))
15:         (buffer-substring beg end)))))

This defun takes a file path as an argument and:

  1. Inserts the contents of the file into a temporary buffer
  2. Navigates to the beginning of the buffer and searches forward for the #+BEGIN_PREVIEW block pattern
    • If it fails to locate this pattern, the defun returns nil
    • If it does locate this pattern, it:
      1. Returns to the beginning of the temporary buffer and repeats the search, recording the location of the block pattern, saving its location under the name beg
      2. Searches forward again for the #+END_PREVIEW pattern, saving its location under the name end
      3. Returns the text between beg and end: the user-selected preview text.

Putting It All Together

Check out the full source code for the site on GitHub.

What's Next?

This site now has everything I need to write more with minimal need to tweak the site configuration every step of the way (which I found myself doing constantly when using Hugo). That said, there are a few more things I want to do before moving this site to its permanent home under my personal domain. These include:

DONE Add navigation buttons to return to the home page/about page/archive page/etc. from any page. (incidentally, if you want to get back home, here's the link).

Update: I've added this in a very simple way. I updated my org-publish-project-alist with:

 1: ;; org-site/build-site.el
 2: (setq org-publish-project-alist
 3:       (list
 4:        (list "org-site:main"
 5:              ;; ...
 6:              :html-preamble (concat "<div class='topnav'>
 7:                                      <a href='/index.html'>Home</a> / 
 8:                                      <a href='/archive.html'>Blog</a> /
 9:                                      <a href='/about.html'>About Me</a>
10:                                      </div>")
11:              ;; ...
12:              )))

DONE Set up a good system for managing images (e.g. for data visualizations from R/Python)

I followed the guide here to set up the export of "static" files using the org-publish-attachment publication function. The new section of my config looks like this:

(setq org-publish-project-alist
       (list "org-site:main"
       ;; ...
       ;; this part is new
       (list "org-site:static"
             :base-directory "./content/"
             :base-extension "css\\|js\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|ogg\\|swf"
             :publishing-directory "./public"
             :recursive t
             :publishing-function 'org-publish-attachment

TODO Set up and test LaTeX exporting for formulas etc.

TODO Add a portfolio page showing some of my past work

TODO Make a more visually interesting home page

Date: 2021-12-03 Fri 00:00

Emacs 27.1 (Org mode 9.3)