Made with Org-Mode
Background
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
Ox-Publish
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:
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:
- Read in each entry's path
- If the entry is not a directory:
- Check if the
my/get-preview
defun returns a value (more onmy/get-preview
later) - Assign the preview text to to the
preview
variable ifmy/get-preview
is not null and assigns"(No preview)"
topreview
otherwise - Format the output as a link to the entry with
(Date) Title
as the description (using theorg-publish-find-date
andorg-publish-find-title
defuns to get the dates/titles of each entry) - include the
preview
(defined above) after a line break
- Check if the
- If the entry is a directory (e.g. if the first condition returns
nil
) and if thesitemap-style
istree
, return the name of the last subdirectory (e.g. the entry/projects/org-site/content/posts
would returnposts
) - 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.
Previews
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 4: 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:
- Inserts the contents of the file into a temporary buffer
- 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:
- 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
- Searches forward again for the
#+END_PREVIEW
pattern, saving its location under the nameend
- Returns the text between
beg
andend
: the user-selected preview text.
- 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
- If it fails to locate this pattern, the defun returns
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 (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 )))