Hello World from Org
I describe yet-another-simple-publishing-setup for a static website using Emacs's Org mode.
skywhi@dreamland:~/blog$ echo "Hello World Wild Web!" > posts/hello.org skywhi@dreamland:~/blog$ make update && make publish skywhi@dreamland:~/blog$ # make blogging great again!
Table of Contents
Rationale
I figured it was finally the time to host some of my content online for sharing, fun, persistence, easy access and all the other usual suspects. Going into this adventure there were only a few requirements that I wanted my website to meet:
- Content should be written in my favorite text editor with Org mode.
- No need for any back-end or front-end logic: serving a static website without any fancy HTML/CSS/JS shenanigans will be more than fine.
- The website should be standalone so that it can be viewed offline as well.
- Convenient hosting: hustling with administrating a web-server for such a small project would not make any sense to me.
Let me explain. I have been using Emacs and Org mode for several years now. Its markup language really fits my needs and the files integrate well with the agenda and other nifty features Org mode has to offer. The ability to hack its behavior directly in elisp is also a great plus to me. This is why I searched for options that would allow me to use my Org content without too much trouble.
When it comes to this website, I want to be able to turn what I am currently working on into publishable content without having to spend extra time on re-formatting my writing completely. It was thus pretty obvious that I would give a try to the built-in org-publish package for Emacs as it is exactly what I am looking for: automated and configurable exporting of Org projects to simple HTML files. There a few other projects which turn a collection of Org files into a website, such as org-static-blog, but I figured I would like to have maximal control over the toolchain and keep external dependencies as low as possible.
Using git versioning gives me the opportunity to track and manage changes over time. Since we are dealing with a static website with exclusively public data, github.io will handle the hosting for me. Whenever I work on a new item, I can pull the remote github repository, create a local branch and start editing. When the content is ready for publishing, the branch is merged into main, pushed to the remote github repository and voilĂ !
Project layout
I ended up with something like the following directory structure for this project. The github repository containing all the source files can be found here.
- ~/org/blog/ - files/ - css/ - fonts/ - img/ - src/ - index.org - about.org - posts/ - hello.org - super-dupa-post.org - templates/ - header.html - footer.html - post.org - readthedocs.org - Makefile - publish-website.el - README.org
src/ contains the standalone Org files that will be published to HTML. They are declaring as few exporting options as possible so that they can easily be used in other contexts or projects. Global exporting options will be defined in publish-website.el and page specific options provided via templates or directly in the files themselves. The files/ and templates/ directories host assets such as stylesheets, fonts, images, etc. that are specific to the HTML export.
The logic behind publishing is implemented in publish-website.el which is then called by a Makefile for convenience. It defines all the directories, files and exporting options to be fed into org-publish. I could have written publish-website.el in literate programming style and use it as source for this blog post, but that would have been a little bit too meta, even for me :)
Finally, README.org is just a symbolic link to src/about.org. Why? Because I can.
To the first page!
Source files
Global exporting options are defined in publish-website.el which can be
overridden by the definitions in the single Org files. When several Org files
share the same defaults, it can make sense to define them in an Org template
file and use the #+SETUPFILE: ../templates/mytemplate.org
directive in the
relevant source files. #+INCLUDE: <path/to/file.org>
can also be used, its
content will then plainly be inserted in the source file before processing.
This is for example the beginning of this post. I set the title, the date, I include a template and insert a preview. More on all this a little bit later.
#+TITLE: Hello World from Org #+DATE: [2025-05-25 Sun] #+begin_preview I describe yet-another-simple-publishing-setup for a static website using Emacs's Org mode. #+end_preview #+begin_src sh skywhi@dreamland:~/blog$ echo "Hello World Wild Web!" > posts/hello.org skywhi@dreamland:~/blog$ make update && make publish skywhi@dreamland:~/blog$ # make blogging great again! #+end_src * Rationale :PROPERTIES: :CUSTOM_ID: rationale :END: I figured it was finally the time to host some of my content online for sharing, fun, persistence, easy access and all the other usual suspects. Going into this adventure there were only a few requirements that I wanted my website to meet: [...]
Appending a CUSTOM_ID
property to headlines makes it possible to link to the
specific headline in the table of contents and HTML.
Templates
Template are used to group seveal Org rendering and exporting that are common to several files. For example, the template under templates/post.org looks like this:
@@html:<div class="post-date">@@ /First published: {{{date(%d %b %Y)}}}/ @@html:</div>@@
The date tag of the post will be extracted and added to the HTML during publishing.
Here is a template for the ReadTheOrg theme by Fabrice Niessen that inserts HTML into any page published with it:
CSS styling
The two files that are used are files/css/style.css and
files/css/htmlize.css. In style.css are all the base styles for the pages
defined. Since I am aiming for a small and flexible website I decided to write
my own CSS manually (and I suck at it tbh). People who don't want to enjoy the
same painful wonderful experience should try to look for pre-existing
stylesheets and exporting options. htmlize.css is used to pretty source code
blocks.
Building and publishing
publish-website.el
It was hyped for long enough, here is the publish-website.el file. This file defines common settings and build instructions for the project. We feed them into org-publish which will take care of producing the corresponding website.
The file is pretty simple and self-explanatory. I mostly added the following
functions to generate a "preview" of each post, which will be visible in the
list of all posts, as well as include a formatted timestamp of when the post was
first added. A custom macro is used to format the timestamp. The content of the
"preview" is written in between the custom #+begin_preview
and #+end_preview
tags.
;; Extract content between #+begin_preview and #+end_preview (defun skw-blog/get-preview (file) "The comments (begin/end_preview) in'file' have to be on their own lines, preferably before and after paragraphs." (with-temp-buffer (message file) (insert-file-contents file) (goto-char (point-min)) (let ((beg (+ 1 (re-search-forward "^#\\+begin_preview$"))) (end (progn (re-search-forward "^#\\+end_preview$") (match-beginning 0)))) (replace-regexp-in-string "\n" " " (buffer-substring beg end))))) ;; Format list of blog post for the sitemap / index (defun skw-blog/org-format-blog-post (entry style project) "Format 'entry' in org-publish 'project' sitemap to include a timestamp." (let ((entry-title (org-publish-find-title entry project))) (if (= (length entry-title) 0) (format "*%s*" entry) (format "{{{timestamp(%s)}}}: [[file:%s][%s]]" (format-time-string "%Y-%m-%d" (org-publish-find-date entry project)) entry entry-title)))) ;; Same but add the content between the "preview" tags (defun skw-blog/org-format-blog-post-with-preview (entry style project) "Format 'entry' in org-publish 'project' sitemap to include a timestamp and preview ('begin/end_preview' tag)." (let ((entry-title (org-publish-find-title entry project)) (preview (skw-blog/get-preview (concat (skw-blog/get-root-directory) "src/posts/" entry)))) ;; dirty (if (= (length entry-title) 0) (format "*%s*" entry) (format "{{{timestamp(%s)}}}: [[file:%s][%s]]\n %s" (format-time-string "%Y-%m-%d" (org-publish-find-date entry project)) entry entry-title preview)))) ;; Exporting macros (setq org-export-global-macros '(("timestamp" . "@@html:<span class=\"timestamp\">$1</span>@@")))
The corresponding publishing rules then look like this:
("website-src" :base-directory ,skw-blog/srcdir :base-extension "org" :exclude ,(regexp-opt '("rss.org")) :recursive t :publishing-directory ,skw-blog/outdir :publishing-function org-html-publish-to-html :auto-sitemap t :sitemap-title ,skw-blog/main-sitemap-title :html-preamble ,skw-blog/header :html-postamble ,skw-blog/footer :html-head ,(concat skw-blog/main-css skw-blog/favicon)) ;; Index of all blog posts ("website-posts-index" :base-directory ,(concat skw-blog/srcdir "posts") :base-extension "org" :exclude ,(regexp-opt '("rss.org" "index.org" "index-no-preview.org")) :publishing-directory ,(concat skw-blog/outdir "posts") :html-preamble ,skw-blog/header :html-postamble ,skw-blog/footer :html-head ,(concat skw-blog/main-css skw-blog/favicon) :auto-sitemap t :sitemap-title "Posts (without preview)" :sitemap-filename "index-no-preview.org" :sitemap-format-entry skw-blog/org-format-blog-post :sitemap-sort-files anti-chronologically) ;; Index of all blog posts with preview ("website-posts-index-preview" :base-directory ,(concat skw-blog/srcdir "posts") :base-extension "org" :exclude ,(regexp-opt '("rss.org" "index.org" "index-no-preview.org")) :publishing-directory ,(concat skw-blog/outdir "posts") :html-preamble ,skw-blog/header :html-postamble ,skw-blog/footer :html-head ,(concat skw-blog/main-css skw-blog/favicon) :auto-sitemap t :sitemap-title "Posts" :sitemap-filename "index.org" :sitemap-format-entry skw-blog/org-format-blog-post-with-preview :sitemap-sort-files anti-chronologically)
The generated ./src/posts/index-no-description.org is inserted into the homepage with:
Makefile
Let's use make to avoid building and managing the website by hand.
OUT_DIR='www' PUBLISH_FILE='publish-website.el' PUBLISH_FUNC='(org-publish "website")' WS_CMD=python -m http.server 12345 --bind localhost --directory $(OUT_DIR) all: rm -rf .cache www emacs -Q --batch --load $(PUBLISH_FILE) --eval $(PUBLISH_FUNC) rm -rf .cache publish: git commit git push -u origin main update: make all make mathjax git add . git status run: $(WS_CMD) .PHONY: all mathjax publish update run
Whenever I want to work on a new item, like adding a new file or editing an
existing one, I switch to a new local branch. I then simply run make
periodically while editing to visualize my changes. When I am done I merge the
branch into my local main branch and run make update
to verify what will be
committed and then make publish
to push the changes on the remote repository.
Cherry on top, make run
spawns a python web-server to view the website locally
at http://localhost:12345.
A few more functionalities
MathJax
MathJax' Org mode support lets me export to HTML math formulas written in LaTeX. Here are a couple of examples using inline code blocks and "proper" LaTeX syntax.
If $a^2=b$ and \( b=2 \), then the solution must be either $$ a=+\sqrt{2} $$ or \[ a=-\sqrt{2} \] Look at this lonely equation I found: \begin{equation} y = x^2+1 \end{equation}
Which will be rendered like this:
If \(a^2=b\) and \( b=2 \), then the solution must be either \[ a=+\sqrt{2} \] or \[ a=-\sqrt{2} \]
Look at this lonely equation I found:
\begin{equation} y = x^2+1 \end{equation}
I install and update regularly MathJax via the Makefile and then enable it
in publish-website.el via the org-html-mathjax-options
variable. Since I
want to be able to view the website offline, I opted for a local install.
mathjax: rm -rf files/js/mathjax git clone https://github.com/mathjax/MathJax.git files/js/mathjax rm -rf files/js/mathjax/.git
(setq org-html-mathjax-options '((path "/files/js/mathjax/es5/tex-mml-chtml.js") (scale 1.0) (align "center") (font "mathjax-modern") (overflow "overflow") (tags "ams") (indent "0em") (multlinewidth "85%") (tagindent ".8em") (tagside "right")))
RSS feed
Generating a RSS to track when new posts are published is actually not that
complicated with ox-rss. Here are a couple of helper functions to generate a
"sitemap" file named rss.org which will list all the entries under
src/posts/ excluding the previously generated indexes. This file is then
converted to XML. The description field of each item is the content of the
preview
tags used earlier.
;; RSS feed generation (defun skw-blog/publish-to-rss (plist filename dir) "Publish 'plist' when 'filename' corresponds to RSS feed Org-file to 'dir'." (if (equal skw-blog/rss-filename (file-name-nondirectory filename)) (org-rss-publish-to-rss plist filename dir))) (defun skw-blog/format-rss-feed (title list) "Generate a sitemap of posts that will be exported as a RSS feed. 'title' is title of the RSS feed and 'list' the files to be included." (concat "#+TITLE: " title "\n\n" (org-list-to-subtree list))) (defun skw-blog/format-rss-feed-entry (entry style project) "Format 'entry' for the posts RSS feed in given 'project'." (let* ((title (org-publish-find-title entry project)) (link (concat (file-name-sans-extension entry) ".html")) (pubdate (format-time-string (car org-time-stamp-formats) (org-publish-find-date entry project))) (preview (skw-blog/get-preview (concat (skw-blog/get-root-directory) "src/posts/" entry)))) (format "%s :properties: :rss_permalink: %s :pubdate: %s :end: %s" title link pubdate preview)))
These functions are then called inside a new publishing rule.
("website-rss" :base-directory ,(concat skw-blog/srcdir "posts") :base-extension "org" :recursive nil :exclude ,(regexp-opt '("rss.org" "index.org" "index-no-preview.org")) :publishing-directory ,skw-blog/outdir :publishing-function skw-blog/publish-to-rss :with-author t :author ,skw-blog/author :email ,skw-blog/email :rss-extension "xml" :rss-image-url ,(concat skw-blog/upstream-url "/files/img/profile.jpg") :html-link-home ,(concat skw-blog/upstream-url "/posts/") :html-link-use-abs-url t :html-link-org-files-as-html t :auto-sitemap t :sitemap-filename ,skw-blog/rss-filename :sitemap-title ,skw-blog/rss-feedname :sitemap-sort-files anti-chronologically :sitemap-function skw-blog/format-rss-feed :sitemap-format-entry skw-blog/format-rss-feed-entry)
Resources
This publishing setup is quite rudimentary. I wrote this note mostly for self reference in case I have to implement a similar project in the future. However, I still hope it can help hesitating people to give a try to org-publish and see if it fits their needs. Maybe existing users could also get a tip or two out of this. The full code is available on my github.
I would encourage anyone fiddling with this to have the official documentation right at hand:
Here are websites that inspired me for this adventure. I highly recommend that you check them out as well as their publishing workflow:
- Building a Emacs Org-Mode Blog by Thomas Ingram showcases a simple yet effective setup.
- Blogging using org-mode (and nothing else) by Dennis Ogbe is a really neat explanations and setup with Org-file pre-processing before publishing.
- A literate programming example by ryuslash.