Blog in Org Mode, Revisited
Table of Contents
I have an earlier post about the same subject. Since then, a lot of things have changed, so I decide to revisit the topic and talk about my improved work flow. This post is a super set of the earlier one, so there is no need to check that out.
The source file of my blog at the time writing can be found here.
1 Why no frameworks?
Personally, I don't like those static site generates, e.g. Hexo, Pelican, Hugo, Jekyll. Each one of them requires you to learn the framework and set it up correctly. It feels like too much work and complexity for a simple static site.
On the other hand, when directly exporting HTML files from Org files, you have the full control of the whole process. And customizing is often trivial.
I'll demonstrate how I build my blog with Org Mode and CSS, and let you decide whether to do the same.
2 Style
I want my blog to be in the old school style like 90's hypertext pages. Some example includes Emacs Lisp Reference Manual, Style Guide for online hypertext, CS166 of Stanford. The simplicity, elegance and plain coolness (what's that?) really attract me, and I hope you, too.
Don't get me wrong, modern web pages like apple.com are beautiful, too. But they are complicated and hard to maintain. I don't feel like spending all the time to make a beautiful animated site while the time could be used to generate better contents. Plus I prefer the "old style" anyway.
3 Implementation
3.1 File structure
The file structure of my blog, root directory is https://archive.casouri.co.uk/note/
index.org
: the Org file for the index pageindex.html
: the exported index pagesetup.org
: my setup file (kind of like template) for Org Mode exportstyle.css
: the style sheet for all the pagesscript.js
: the script file for all the pages. Currently I don't have anything in there.- year(e.g. 2918)
- post: each post is in a separate directory
index.org
: the Org fileindex.html
: the exported HTML file- other static files used in the page
- post: each post is in a separate directory
3.2 Links
Because I want my site to work both online and on disk (that is, you can download the site to disk and view it the same), I use relative links everywhere.
One inconvenience is that I can't use link that points to a
directory anymore: say ./2018/mypost/
. Instead, I
need to explicitly write out the file:
./2018/mypost/index.html
. This looks a little bit
dangerous, but should be OK.
For internal links, just use the headline name of the headline you want to reference as the link. So
[[Template (Sorf of)][Headline below me]]
will point to the headline below.
3.3 Template (sort of)
This is my template:
#+OPTIONS: html-style:nil #+HTML_HEAD: <link rel="stylesheet" type="text/css" href="../../style.css"/> #+HTML_HEAD_EXTRA: <script type="text/javascript" src="../../script.js"></script> #+HTML_HEAD_EXTRA: <link rel="icon" type="image/png" href="../../../favicon.png"> #+HTML_LINK_UP: ../../index.html #+HTML_LINK_HOME: ../../index.html #+OPTIONS: toc:2
It is called setup file in Org Mode. In
index.org
file of each post, there is a line
#+SETUPFILE: ../../setup.org
. When Org exports the
file, it first loads the setup file (setup.org
),
and environment set by that file will be used when exporting
the post. You can think of it as adding these lines to every
Org file before exporting.
The purpose of each line:
#+OPTIONS: html-style:nil
ꜛ disable the default styling that Org HTML exporter uses. I style my blog in my own CSS file.
#+HTML_HEAD: <link rel="stylesheet" type="text/css" href="../../style.css"/>
ꜛ Link to my style sheet.
#+HTML_HEAD_EXTRA: <script type="text/javascript" src="../../script.js"></script>
ꜛ Link to my script file.
#+HTML_HEAD_EXTRA: <link rel="icon" type="image/png" href="../../../favicon.png">
ꜛ Link to my favicon.
#+HTML_LINK_UP: ../../index.html #+HTML_LINK_HOME: ../../index.html
ꜛ Add UP and HOME link to head line. See more below.
#+OPTIONS: toc:2
ꜛ Collect down to the second level header for TOC.
3.4 TOC
On narrow screens, the table of content will simply be on top of the body. On wider screens, I made it to float on the right. If you are reading this post on a PC, you can probably see it.
It is achieved by this CSS snippet:
@media screen and (min-width: 800px) { /* floating TOC */ #table-of-contents { font-size: 12pt; bottom:0; position:fixed; overflow-y:scroll; overflow-x:hidden; top: 5%; right: 2%; width: 20%; } /* centered content */ body { margin-left: 10%; margin-right: 30%; /* this way floating TOC wouldn't touch content */ width: 58%; } }
You can see that on wider screens, the content only occupies 60% (actually 58%) of the width of the screen.
For the TOC, overflow-y:scroll;
makes TOC
scroll able in case TOC is height is larger than the screen
height.
3.5 Head line
Update
:I made all pages to have the modified headline. See below.
The head line is the strip on the very top of each page.
Specifically the line UP | HOME
on posts and
UP | HOME RSS | Source | License
on the index page.
The normal behavior of it is UP | HOME
. Org
HTML exporter adds this head line when you have
#+HTML_LINK_UP: path-up-a-level #+HTML_LINK_HOME: path-to-home
in your setup. As you have already seen, I have these configured in my setup file.
For the index page, however,
I hacked it a little bit. In index.org
of the
index page, I have this snippet
in the end of the file:
# Local Variables:
# org-html-home/up-format: "<div id=\"org-div-home-and-up-index-page\"> <div> <a accesskey=\"h\" href=\"%s\"> UP </a> | <a accesskey=\"H\" href=\"%s\"> HOME </a> </div> <div> <a href=\"./index.xml\"> RSS </a> | <a href=\"https://github.com/casouri/casouri.github.io\"> Source </a> | <a href=\"https://creativecommons.org/licenses/by-sa/4.0/\"> License </a> </div> </div>"
# End:
That is a
file local variable, it sets
org-html-home/up-format
to
<div id="org-div-home-and-up-index-page"> <div> <a accesskey="h" href="../home/index.html"> UP </a> | <a accesskey="H" href="../home/index.html"> HOME </a> </div> <div> <a href="./index.xml"> RSS </a> | <a href="https://github.com/casouri/casouri.github.io"> Source </a> | <a href="https://creativecommons.org/licenses/by-sa/4.0/"> License </a> </div> </div>
and in effect, injects RSS | Source | License
part into the format.
To make the two part align with either side, I set the style
of org-div-home-and-up-index-page
as
div#org-div-home-and-up-index-page { /* headline */ display: flex; justify-content: space-between; }
See here for more on CSS flex box.
3.6 RSS
RSS feed is an essential part of a blog. However, Org Mode doesn't make it easy to add one for my blog.
Some references that helped me along the way:
- Creating an RSS feed with Org mode
- Org Export Reference Documentation
- RSS 2.0 SPECIFICATION
- Macro replacement
I use a modified ox-rss.el to generate RSS file. As its name suggests, it is a contrib package for Org Mode, so you need to download it first.
3.6.1 What does
ox-rss.el
do
ox-rss.el
exports each first-level header in
the current file to an entry of RSS file. The description of
each entry is whatever inside the header.
It adds ID
, PUBDATE
to each
header if none exists. ID
is a pointer to the
header so it can build a link that points to the header in
RSS file. This is not useful for me because each headline in
the index page is really just
a link to my post with a short description. The actual
content is not there. For that matter, I use
RSS_PERMALINK
to set the link manually.
PUBDATE
is the publication date of the post.
So a header will look like this (the backslash on the first line is for escaping asterisk after it):
\* [[./2018/this-is-my-post/index.html][This Is My Post]] :COOL: :PROPERTIES: :ID: some-id-afnoef73r3rb3rv3l :PUBDATE: <2018-11-16 Fri> :RSS_PERMALINK: https://archive.casouri.co.uk/note/2018/this-is-my-post/index.html :END: This is my post. It's cool.
Some issues:
- That's a lot of typing
- I'm repeating the path to my post and the root url of my blog, that's not DRY.
- I have
DATE
set in each post'sindex.org
. And I don't feel like manually typing them here. That isn't DRY, either.
On top of that, ox-rss
does something not so
good with RSS_PERMALINK
: it prefixes my link
with path of UP or HOME if they exists. In my case they do,
and the final url becomes
../index.htmlhttps://archive.casouri.co.uk/note/path/to/my/post/index.html
.
3.6.2 My modification
I don't want to modify the default behavior of
ox-rss.el
, so I added two properties —
RSS_BASE_URL
and RSS_RELATIVE_LINK
.
And modified the source of ox-rss.el
:
;; In `org-rss-headline' (let (... (hl-rel (org-element-property :RSS_RELATIVE_LINK headline)) (url-base (org-element-property :RSS_URL_BASE headline))) ... (publink (or (and hl-rel url-base (concat url-base hl-rel)) (and hl-perm (concat (or hl-home hl-pdir) hl-perm)) (concat (or hl-home hl-pdir) (file-name-nondirectory (file-name-sans-extension (plist-get info :input-file))) "." htmlext "#" anchor))) htmlext "#" anchor))))
And the header would look like
\* [[./2018/this-is-my-post/index.html][This Is My Post]] :COOL: :PROPERTIES: :ID: some-id-afnoef73r3rb3rv3l :PUBDATE: <2018-11-16 Fri> :RSS_BASE_URL: https://archive.casouri.co.uk/note/ :RSS_RELATIVE_LINK: 2018/this-is-my-post/index.html :END: This is my post. It's cool.
3.6.3 Macro make it DRY
It works now, but the issues 1, 2, 3 are still not resolved. For that, we can use a macro to do the typing for us.
With macro post
, above text shrinks to
{{{post(This Is My Post,2018/this-is-my-post/,:COOL:)}}} This is my post. It's cool.
I have a command1 to type even that for me, so all I need is type the title: "This Is My Post".
#+MACRO: post (eval (format "* [[./$2index.html][$1]] $3\n :PROPERTIES:\n :RSS_RELATIVE_LINK: $2\n :RSS_URL_BASE: https://archive.casouri.co.uk/note/\n :PUBDATE: %s\n :END:" (let ((buffer (find-file-noselect "$2index.org")) date) (setq date (with-current-buffer buffer (plist-get (car (cdr (car (plist-get (org-export-get-environment) :date)))) :raw-value))) (kill-buffer buffer) date)))
I know looks like heap of crap, here is the code prettied:
$1
is the first argument — the title,
$2
is the path, $3
are the
tags.
(eval (format "* [[./$2index.html][$1]] $3 :PROPERTIES: :RSS_RELATIVE_LINK: $2 :RSS_URL_BASE: https://archive.casouri.co.uk/note/ :PUBDATE: %s :END:" (let ((buffer (find-file "$2index.org")) date) (setq date (with-current-buffer (plist-get (car (cdr (car (plist-get (org-export-get-environment) :date)))) :raw-value))) (kill-buffer buffer) date)))
The (with-current-buffer ...)
part opens the
post's index.org
file and extracts the date
out.
3.7 Tag filters for index page
(Updated on
)I have tags on the right of each header on the index page. You can't click them, though.
Normally when a blog has tags, you can click one, and it brings you to a page listing all the posts with that tag. I didn't go along with that approach, but make a filter button for each tag. Selecting and de-selecting each tag will hide and show posts with that particular tag on the index page. It's pretty cool.
Initially I made the buttons to have three states:
include
, noselect
, and
exclude
. include
and
noselect
are normal selecting and de=selecting.
excluede
means “don't show posts with this tag,
not even when the post has a tag that is in
include
state”.
I figure it would probably confuse people and it's use case is pretty limited; so I removed it.
The idea is, each time a button is clicked, toggle it's state (implemented with class attribute) and add/remove it from "included tags list" (initial every tag is in the list). Then scan through the DOM and display/hide according to "included tags list".
I put the HTML, CSS and JavaScript in 3.11.
3.8 Publish
(Updated on
)I write this publish function so I don't need to export by hand. The function only export when org file is newer than html file.
Another benefit of publish function is that I can add custom
environment variables before export. I set
org-html-home/up-format
and
org-html-postamble-format
to custom values.
(defvar moon-org-html-postamble-format '(("en" "<p class=\"author\">Written by %a <%e></p> <p class=\"first-publish\">First Published on %d</p> <p class-\"last-modified\">Last modified on %C</p>"))) (defvar moon-org-html-home/up-format "<div id=\"org-div-home-and-up-index-page\"> <div> <a accesskey=\"h\" href=\"%s\"> UP </a> | <a accesskey=\"H\" href=\"%s\"> HOME </a> </div> <div> <a href=\"./index.xml\"> RSS </a> | <a href=\"https://github.com/casouri/casouri.github.io\"> Source </a> | <a href=\"https://creativecommons.org/licenses/by-sa/4.0/\"> License </a> </div> </div>") (defvar moon-publish-root-dir "~/p/casouri/note/") (require 'f) (defun moon/publish (&optional force) "Publish my blog. If FORCE is non-nil, only export when org file is newer than html file." (interactive) (dolist (dir (f-directories moon-publish-root-dir)) (dolist (post-dir (f-directories dir)) (moon-html-export post-dir force))) (require 'ox-rss) (moon-html-export moon-publish-root-dir force) (let ((buffer (find-file (expand-file-name "index.org" moon-publish-root-dir)))) (with-current-buffer buffer (org-rss-export-to-rss)) (kill-buffer buffer))) (defun moon-html-export (dir &optional force) "Export index.org to index.html in DIR is the latter is older. If FORCE is non-nil, only export when org file is newer than html file." (moon-load-theme 'doom-one-light) (let ((org-html-postamble-format moon-org-html-postamble-format) (org-html-postamble t) (org-html-home/up-format moon-org-html-home/up-format) (org-file (expand-file-name "index.org" dir)) (html-file (expand-file-name "index.html" dir))) (when (or force (file-newer-than-file-p org-file html-file)) (let ((buffer (find-file org-file))) (with-current-buffer buffer (org-html-export-to-html)) (kill-buffer)))) (moon-load-theme 'doom-cyberpunk))
3.9 Other CSS tricks
3.9.1 Code block
code, .example, .src { padding: 3px; background-color: #F4F6F6; font-size: 12pt; overflow-x: scroll; }
3.9.2 Tags
span.tag span { /* headline tags */ font-size: 12pt; border-width: 2px; border-style: solid; } code { white-space: nowrap; }
3.9.3 Footnote
.footdef { /* make footnote number and content to be on th same line */ display: flex; }
3.9.4 Image size
I limit the image size to 600px width:
img { max-width: 600px; }
3.10 Misc
3.10.1 Syntax highlight
Syntax highlight takes the current font-lock color for the exported HTML. So switch to a light theme for reasonable syntax colors.
3.11 Filter code
<div id="taglist"> <p onclick="toggleAll()" id="tagAll">All</p> <p onclick="toggleTag(this)">Emacs</p> <p onclick="toggleTag(this)">Org_Mode</p> <p onclick="toggleTag(this)">Web</p> <p onclick="toggleTag(this)">Programming</p> <p onclick="toggleTag(this)">Network</p> <p onclick="toggleTag(this)">Music</p> <p onclick="toggleTag(this)">Design</p> <p onclick="toggleTag(this)">Anime</p> <p onclick="toggleTag(this)">Hacker</p> </div>
/* desktop, tablet landscape */ @media screen and (min-width: 1025px) { div#taglist { position: fixed; overflow-y: scroll; overflow-x: wrap; top: 40pt; left: 2%; /* width: 20%; */ } div#taglist p { /* make cursor hand on hover */ cursor: pointer; margin-top: 20pt; border-width: 2px; border-style: solid; padding-left: 1em; padding-right: 1em; text-align: right; } div#taglist p:hover { background-color: black !important; color: white !important; } div#taglist p.noselect { color: gray; border-color: gray; } div#taglist p.include { color: black; border-color:black } div#taglist p.exclude { text-decoration: line-through; } }
I commented out the exclude part, if you like it, you can put it back in.
function myremove(lst, elt) { var index = lst.indexOf(elt) if (index > -1) { lst.splice(index, 1) } } // tag filtering window.onload = setupTagList var excludeTagList = [] var includeTagList = [] var allTagList = [] function setupTagList() { for (var tag of document.getElementById('taglist').children) { tag.className = 'include' includeTagList.push(tag.innerHTML) allTagList.push(tag) } } function toggleAll() { toggleTag(document.getElementById('tagAll')) for (tag of allTagList) { while (tag.className !== tagAll.className) { toggleTag(tag) } } } function toggleTag(tag) { switch (tag.className) { case 'include': var nextState = 'noselect' myremove(includeTagList, tag.innerHTML) break case 'noselect': // var nextState = 'exclude' // excludeTagList.push(tag.innerHTML) var nextState = 'include' includeTagList.push(tag.innerHTML) break // case 'exclude': // var nextState = 'include' // myremove(excludeTagList, tag.innerHTML) // includeTagList.push(tag.innerHTML) // break } tag.className = nextState filterHeaders() } function filterHeaders() { for (var header of document.getElementById('content').children) { if (header.className === "outline-2") { for (var tag of header.getElementsByClassName('tag')[0].children) { if (includeTagList.includes(tag.innerHTML)) { header.style.display = 'block' break } else { header.style.display = 'none' } } // exclude list overrides include list for (var tag of header.getElementsByClassName('tag')[0].children) { if (excludeTagList.includes(header.tagName)) { header.style.display = 'none' } } } } }
Footnotes:
The command also creates files and folders for me and types the necessary options for me. Here is the code:
(defun moon/new-blog (title) "Make a new blog post with TITLE." (interactive "M") (let* ((year (shell-command-to-string "echo -n $(date +%Y)")) (dir-file-name (downcase (replace-regexp-in-string " " "-" title))) (dir-path (concat (format "~/p/casouri/note/%s/" year) dir-file-name)) (file-path (concat dir-path "/index.org"))) (mkdir dir-path) (find-file file-path) (insert (format "#+SETUPFILE: ../../setup.org #+TITLE: %s #+DATE: " title)) (kill-new (format "{{{post(%s/%s/,%s)}}}" title year dir-file-name)) (save-buffer) (find-file "~/p/casouri/note/index.org")))
The manual doesn't mention that you can
use (eval)
inside macros. Note that if you use
(eval)
, the whole macro definition has to be in
(eval)
:
#+MACRO naive-macro something (eval "like this") doesn't work.
That will just expand to
something (eval "like this") doesn't work.
On the other hand,
#+MACRO reasonable-macro (eval "Something like this works.")