UP | HOME
[2016-11-13 Sun 00:00 (last updated: 2018-08-19 Sun 19:01) Press ? for navigation help]

Personal website in org

Table of Contents

Introduction

This post describes the configuration for this website, which is statically generated using emacs and org-mode. Org-mode's publishing functionality is used to generate the HTML content from source org files. Here is a list of existing examples that I have used to build this website:

As for other posts in this website, the actual configuration files, including emacs-lisp configuration, CSS, etc. can be generated by tangling the org-file (org-babel-tangle bound to C-c C-v t by default).

The folder structure is similar to the one described in the Worg tutorial: the website source is composed of org files with the following structure:

tree -a org/ -I "*~"
org/
├── blog.org
├── css
│   └── site.css
├── html
│   ├── html_head.html
│   ├── html_postamble.html
│   └── html_preamble.html
├── images
│   ├── 2016-10-05-Awesome-wm_configuration
│   │   ├── desktop-cmus-popup.png
│   │   ├── desktop-confirm_action.png
│   │   ├── desktop-global_prompt-help.png
│   │   ├── desktop-main.png
│   │   └── desktop-wibox.png
│   ├── 2016-11-13-Personal_website_in_org
│   │   └── website-post.png
│   └── feed-icon-28x28.png
├── index.org
├── js
│   └── org-info.js
├── posts
│   ├── 2016-10-05-Awesome-wm_configuration.org
│   └── 2016-11-13-Personal_website_in_org.org
├── rss.org
├── setup.org
└── sitemap.org

7 directories, 19 files

The start page is the index.org file, which is described later. It includes links to the different pages, and a list of recent blog posts. Blog posts are located in the posts/ folder, where each file contains a single post and the filename is prefixed by the post date. Attachments for blog posts (e.g. images) are placed in the images/ folder with a subfolder corresponding to the post filename. Blog posts are pushed to the blog from regular org files using a custom function which handles file naming and attachments. Finally, the static content is located in the css/, js/ and html/ folders.

The workflow I use to update this website is as follows:

  • for basic webpages (i.e. everything but the blog posts), edit the org files in the source tree,
  • for blog posts, create a standalone org file outside of the website source tree, then push it using the push-to-blog function, which handles file links and adds boilerplate options to the org file before moving the processed org file to the website source tree,
  • finally run the publishing wrapper function to generate the website,
  • optionally push the changes to the actual server (e.g. github pages).

Style

The styling of the website is done in CSS, the goal being to make it modular rather than aesthetically pleasing (as visible on Fig. 1). When tangled, this section generates the site.css used for styling.

website-post.png

Figure 1: Screenshot of a post.

The website design is "responsive": the layout varies based on the client's size. Three client sizes are considered: large (width \(\geq\) 1024 px), medium (600 px \(\leq\) width \(<\) 1024 px) and small (width \(<\) 600 px).

Variables

Most of the properties used for styling are defined at the top of the style file. This could be done via org variables passed to the CSS source block, but ob-css does not support this (it seems that it could be added with minimal effort for scalar variables at least).

[org/css/site.css]
/** CSS variables */ :root { /* Body */ /* ---- */ --body-bg: white; --content-padding: 10px; --content-bg: #F1F1F1; --content-box-shadow: 0; --body-margin: 30px; /* not used for small screens */ /* Footer */ /* ------ */ --footer-border: 1px solid black; --footer-padding: 10px; --footer-buttons-bg: #4C6FB0; --footer-buttons-bg-hover: navy; --footer-buttons-padding: 5px 10px; --footer-buttons-color: white; --footer-buttons-font-size: 90%; --footer-buttons-border: 2px solid navy; --footer-buttons-min-width: 270px; /* Table of contents */ /* ----------------- */ /* Basic style */ --toc-bg: #f1f1f1; --toc-box-shadow: 0 0 0.8em #777777; --toc-border-radius: 5px; --toc-li-color: black; --toc-hover-bg-color: #555; --toc-hover-color: white; /* Layout */ --toc-nav-vspacing: 20px; --toc-base-padding: 5px; --toc-ul-padding-left: 20px; --toc-local-padding: 5px; --toc-extra-padding-x: 4px; /* Navigation bar */ /* -------------- */ /* Basic style */ --nav-bg: #333; --nav-hover: #111; /* RSS */ /* --- */ --rss-img: url("/blog/images/feed-icon-28x28.png"); /* Tables */ /* ------ */ --tbl-border-color: #4E4E4F; --tbl-border-size: 1px; /* Org-js */ /* ------ */ --org-js-console-bg: var(--footer-buttons-bg); /* Fonts */ /* ----- */ /* Text */ --font-body-family: Arial, sans-serif; --font-body-size: 14pt; /* Code */ --font-code-family: DejaVu, Lucida Console, monospace; --font-code-color: #4E4E4F; --font-code-size: 11pt; --font-src-family: DejaVu, Lucida Console, monospace; --font-example-family: DejaVu, Lucida Console, monospace; --font-example-size: 11pt; /* Links */ --font-a-color: navy; --font-a-coderef-color: #7082BC; /* Color scheme */ /* ------------ */ /* Source blocks */ --src-bg: #fBF9E7; --lua-bg: var(--src-bg); } /** Responsive variables */ /* Large screens */ @media (min-width: 1024px) { :root { --side-width: 250px; --body-width: calc(92% - var(--side-width)); --nav-base-font-size: 80%; --nav-height: 50px; --nav-padding: 14px 16px; --toc-base-font-size: 70%; --toc-ul-font-size: 150%; --toc-ul-local-font-size: 130%; --toc-ul-2-font-size: 80%; --toc-ul-3-font-size: 80%; --font-src-size: 11pt; --rss-padding: 0 0 0 25px; --rss-size: 25px 25px; --rss-pos: 50%; } } /* Medium screens */ @media (min-width: 600px) and (max-width: 1024px) { :root { --side-width: 150px; --body-width: calc(92% - var(--side-width)); --nav-base-font-size: 70%; --nav-height: 30px; --nav-padding: 5px 6px; --toc-base-font-size: 70%; --toc-ul-font-size: 130%; --toc-ul-local-font-size: 130%; --toc-ul-2-font-size: 80%; --font-src-size: 11pt; --rss-padding: 0 0 19px 9px; --rss-size: 10px 10px; --rss-pos: 25%; } } /* Small screens */ @media (max-width: 600px) { :root { --side-width: 150px; /* unused in this case */ --body-width: 100%; --nav-base-font-size: 80%; --nav-height: 30px; --nav-padding: 5px 6px; --toc-base-font-size: 80%; --toc-ul-font-size: 120%; --toc-ul-local-font-size: 130%; --toc-ul-2-font-size: 80%; --font-src-size: 10pt; --rss-padding: 0 0 19px 20px; --rss-size: 20px 20px; --rss-pos: 19%; } }

Table of contents

Exported pages use org-infojs for navigation. It comes with a nice table of contents for the document (with CSS id #table-of-contents). Similar to the fixed table of contents used on the Worg website, it is fixed on one side of the page.

A local table of contents for each section is also used (since blog posts tend to be quite lengthy), and styled in a similar fashion. The relevant CSS class is .org-info-js_local-toc.

The table of contents is "responsive" as it adapts to the viewport (client) size:

  • Fixed vertical menu for large screens
  • Reduce to single level table on medium size screens
  • Collapse to regular section (floating) for small screens

Basic style

First, we define the basic styling properties for the table of contents: color, shadow, size, etc.

[org/css/site.css]
/** Table of content */ /* Basic styling (color, font, shadow) common to global and local TOC */ #table-of-contents, .org-info-js_local-toc { background-color: var(--toc-bg); box-shadow: var(--toc-box-shadow); border-bottom-left-radius: var(--toc-border-radius); font-size: var(--toc-base-font-size); } /* Size (global TOC only) */ #table-of-contents { width: var(--side-width); } .org-info-js_local-toc { padding: var(--toc-local-padding); }

Position

The placement of the table of contents depends on the screen size: the different definitions are placed here.

Large screen

For medium and large screens, the table of contents is fixed on the left of the screen.

[org/css/site.css]
/* Fixed vertical table of content */ @media screen and (min-width: 600px) { #table-of-contents { position: fixed; left: 0; top: calc(var(--nav-height) + var(--toc-nav-vspacing)); bottom: var(--toc-nav-vspacing); padding: var(--toc-base-padding); overflow: auto; } }
Medium screen

For medium and small screens, the table of contents is limited to only top level headings.

[org/css/site.css]
@media screen and (max-width: 1024px) { #table-of-contents > div > ul > li > ul { display: none; } }
Small screen

For small screens, the table of contents is a regular section, and it is set to use (almost) all the available width.

[org/css/site.css]
@media screen and (max-width: 600px) { #table-of-contents { position: relative; width: 90%; margin: auto; } }

List style

The style for entries in the table of contents are defined here. The following block sets font sizes and alignment.

[org/css/site.css]
#table-of-contents ul, .org-info-js_local-toc ul { list-style-type: none; margin: 0; padding-left: var(--toc-ul-padding-left); } #table-of-contents ul { font-size: var(--toc-ul-font-size); } .org-info-js_local-toc ul { font-size: var(--toc-ul-local-font-size); } #table-of-contents > div > ul, .org-info-js_local-toc > div > ul { list-style-type: none; margin: 0; padding-left: var(--toc-extra-padding-x); } #table-of-contents > div > ul > li > ul{ font-size: var(--toc-ul-2-font-size); vertical-align: middle; } #table-of-contents > div > ul > li > ul > li > ul{ font-size: var(--toc-ul-3-font-size); vertical-align: middle; }

The appearance of links is defined here:

[org/css/site.css]
#table-of-contents li a, .org-info-js_local-toc li a { display: block; color: var(--toc-li-color); text-decoration: none; } #table-of-contents li a { vertical-align: middle; } #table-of-contents li a:hover:not(.active), .org-info-js_local-toc li a:hover:not(.active) { background-color: var(--toc-hover-bg-color); color: var(--toc-hover-color); }

Navigation bar

A small navigation bar is added to all pages in the website (on the top left corner). It contains links to the home page, a list of blog posts and the RSS feed. As for the table of contents, its sizes decreases for medium size clients, and collapses to a floating menu at the top for small clients.

Basic style

First, the basic style elements are defined, including the position, size, color and text alignment.

[org/css/site.css]
/** Navigation bar */ .topnav { top: 0; left: 0; max-height: var(--nav-height); vertical-align: middle; background-color: var(--nav-bg); width: var(--side-width); font-size: var(--nav-base-font-size); } /* RSS icon */ .feed { margin-left: 3px; padding: var(--rss-padding); background: var(--rss-img) no-repeat 0 var(--rss-pos); background-size: var(--rss-size); }

Position

Medium and large screen

For medium and large screens, the navigation bar is fixed on the top left corner.

[org/css/site.css]
@media screen and (min-width: 600px) { .topnav { position: fixed; } }
Small screen

For small screens, the navigation bar is placed at the top, and made floating.

[org/css/site.css]
@media screen and (max-width: 600px) { .topnav { position: relative; width: 100%; } }

List style

The following block sets the style of the navigation items.

[org/css/site.css]
.topnav ul { list-style-type: none; margin: 0; padding: 0; overflow: hidden; position: relative; vertical-align: middle; } .topnav li { float: left; vertical-align: middle; position: relative; }
[org/css/site.css]
.topnav li a { display: block; color: white; text-align: left; padding: var(--nav-padding); text-decoration: none; position: relative; vertical-align: middle; } .topnav li a:hover:not(.active) { background-color: var(--nav-hover); } .topnav .right { float: right; }

Source blocks

This section defines the appearance of source blocks in the rendered HTML. The main features are:

  • allow scrollbars on overflow,
  • move the tooltip with the language inside the div block,
  • set colors for the background and the filename label (see the source labeling setup section).
[org/css/site.css]
pre.src { overflow-x: auto; overflow-y: auto; line-height: 2.5ex; max-height: 125ex; } pre.src:before { visibility: hidden; top: 0; left: 0; /* max-width: 50px; */ /* opacity: 0.5; */ /* border: none; */ /* background-color: transparent; */ } /* Source block color based on file type */ pre.src.src.src-lua { background-color: var(--lua-bg); } /* Source block label */ .src-label { color: darkgray; }

Tables

The following defines the style of HTML tables:

[org/css/site.css]
/* HTML tables */ table, th, td { border-color: var(--tbl-border-color); border: solid var(--tbl-border-size); }

Org-js

The org-infojs tool inserts a search box in the rendered html. It is used for search operations. The following sets the default location for the search box.

[org/css/site.css]
#org-info-js_console-container { bottom: 0; margin-top: 0; }

Body

The following CSS controls the size of the body, which adapts to the client size.

[org/css/site.css]
/* Body */ body { width: var(--body-width); overflow-x: hidden; background-color: var(--body-bg); } @media (min-width: 600px) { body { margin-left: calc(var(--side-width) + var(--body-margin)); margin-right: var(--body-margin); } } @media (max-width: 600px) { body { margin-left: 0; margin-right: 0; } }

Preamble

The preamble is a text added to the top of the page. The actual content is defined in the HTML preamble section.

[org/css/site.css]
/* Text before post (date and help) */ .foreword { color: gray; font-size: 80%; }

The home=/=up buttons are hidden for small clients.

[org/css/site.css]
@media screen and (max-width: 600px) { #org-div-home-and-up { display: none; } }

Content

The following block is used to limit the width of the main content and add a background color.

[org/css/site.css]
/* Main content */ #content { background-color: var(--content-bg); box-shadow: var(--content-box-shadow); padding-left: var(--content-padding); padding-right: var(--content-padding); }

Postamble

This section defines the layout of the HTML postamble used on all pages.

[org/css/site.css]
#postamble { padding: var(--footer-padding); border: var(--footer-border); }

Fonts

Here, the fonts for text, links and code are selected.

[org/css/site.css]
/* Fonts */ body { font-family: var(--font-body-family); font-size: var(--font-body-size); } code { font-family: var(--font-code-family); font-size: var(--font-code-size); color: var(--font-code-color); } pre.src { font-family: var(--font-src-family); font-size: var(--font-src-size); background-color: var(--src-bg); } pre.example { font-family: var(--font-example-family); font-size: var(--font-example-size); } /* Links */ a { text-decoration: none; color: var(--font-a-color); } a.coderef { color: var(--font-a-coderef-color); }

HTML tags

Buttons

The HTML postamble uses buttons to view the page org source; their appearance is defined here:

[org/css/site.css]
.footer-button { border: none; text-align: center; text-decoration: none; display: inline-block; background-color: var(--footer-buttons-bg); color: var(--footer-buttons-color); padding: var(--footer-buttons-padding); font-size: var(--footer-buttons-font-size); border: var(--footer-buttons-border); min-width: var(--footer-buttons-min-width); } .footer-button:hover { background-color: var(--footer-buttons-bg-hover); }

Keyboard shortcut (<kbd>)

Define a simple css style for the html <kbd> tag.

[org/css/site.css]
kbd { font-family: DejaVu, Lucida Console, monospace; font-size: 85%; display: inline-block; padding: 0.5px 5px; color: #616161; vertical-align: middle; background-color: #fefefe; border: solid 1px #616161; border-radius: 4px; box-shadow: 0 0 12px 2px #dbdbdb inset, 0 4px 1px -2px #dbdbdb; }

Common HTML

The org publishing mechanism supports inclusion of common HTML at the beginning and the end of each published page, as described here.

Head

In each page's <head> section, a few items are added:

[org/html/html_head.html]
<link rel='stylesheet' href='/blog/css/site.css' type='text/css'/> <script type="text/javascript"> function rpl(expr, a, b) { var i = 0; while (i != -1) { i = expr.indexOf(a, i); if (i >= 0) { expr = expr.substring(0, i) + b + expr.substring(i + a.length); i += b.length; } } return expr } function show_org_source(flag_html = false){ //document.location.href = rpl(document.location.href, "html", "org"); var re = /[\/]$/g; var html_url = document.location.href.replace(re, '/index.html'); var out_ext = flag_html? "org.html": "org"; window.open(rpl(html_url, "html", out_ext), '_blank'); } </script>

Preamble

The preamble contains the navigation bar with links to the website home, posts and RSS. It also adds a short text before the webpage's content with the file date and a pointer to access the help menu of the org-infojs script.

[org/html/html_preamble.html]
<div class='topnav'> <ul> <li><a href='/blog/index.html'>Home</a></li> <li><a href='/blog/blog.html'>Blog</a></li> <li class="right feed"><a href='/blog/rss.xml'>RSS</a></li> </ul> </div> <div class="foreword"> [%d (last updated: %C) Press <kbd>?</kbd> for navigation help] </div>

Postamble

The postamble is composed of a simple block showing the emacs and org-mode versions used to build the page, along with buttons to access the source org file, and an htmlized version. The raw version should allow to reproduce the output published on the website.

[org/html/html_postamble.html]
<div id="show_source"> <input type="button" class="footer-button" value="Show Org source (html)" onClick='show_org_source(true)' /> <input type="button" class="footer-button" value="Show Org source (raw)" onClick='show_org_source()' /> </div> <div class='footer'> Last updated %C. <br> Built with %c. </div>

Main page

The main page of the website is minimalist, it lists the most recent blog posts by including the top of the sitemap and links to the full list of blog posts and the sitemap itself.

[org/index.org]
#+TITLE: misc/ #+DATE: <2016-10-02 Sun> Random articles about linux, emacs, data processing, etc. This site and the articles are built with emacs and org-mode (using org-js). * Blog #+INCLUDE: sitemap.org::*posts :lines "-5" :only-contents t Older posts * Sitemap

Blog page

The blog page simply lists all the posts using the sitemap.

[org/blog.org]
#+TITLE: Blog posts All the blog posts: #+INCLUDE: sitemap.org::*posts :only-contents t

Org header

The setup.org file contains default headers added to the top of each org file pushed as a blog post using the push-to-blog function. The additional header performs the following:

  • setup the path to the org-infojs source (local) and the table of contents defaults,
  • add the :comments link header argument to allow for edits in the tangled file and use of org-detangle,
  • prevents evaluation of source blocks by default,
  • define common macros
[org/setup.org]
#+INFOJS_OPT: toc:t tdepth:2 view:showall ftoc:t path:/blog/js/org-info.js #+PROPERTY: header-args :eval never #+PROPERTY: header-args+ :comments link #+STARTUP: nohideblocks #+MACRO: tt \nbsp{} #+MACRO: kbd call_el-common-kbd[:eval yes :results value raw :exports results]($1)

Along with header specifications, common source blocks are also included in each file via the source-blocks.org file.

[org/source-blocks.org]
#+NAME: el-common-kbd #+BEGIN_SRC emacs-lisp :var x="" :exports none (format "@@html:%s@@" (mapconcat (lambda (el) (format "<kbd>%s</kbd>" el)) (split-string x " ") "&nbsp;")) #+END_SRC

Emacs setup

This section contains the emacs-lisp backend code used to produce the website from the set of source org files. While the org-publish mechanism handles most of this, some specific items are handled manually.

When tangled, this section creates the my-website.el file which can then be loaded from the emacs setup file (using (require 'my-website)).

Header

This first section creates the header for the my-website.el file.

[my-website.el]
;;; my-website.el --- Personal website configuration ;;; Commentary: ;; Defines functions for publishing a static website. This file was produced ;; automatically from website.org. See the website.org file for a full ;; description. ;;; Code:

Sitemap

The sitemap is used here to provide an automatically updated list of blog posts.

The idea is to generate a sitemap.org file which contains a subtree for blog posts, sorted by date (most recent first). Then, the org-mode #+INCLUDE directive is used to display the blog posts (see the documentation) on any page. A list of the 5 most recent posts is shown on the main page and a full list is included in the blog page. This keeps lists of posts up-to-date.

[my-website.el]
(defun ™/org-publish-org-sitemap (title list) "Sitemap generation function." (concat "#+TITLE: Sitemap\n\n" (org-list-to-subtree list))) (defun ™/org-publish-org-sitemap-format (entry style project) "Custom sitemap entry formatting: add date" (cond ((not (directory-name-p entry)) (format "[[file:%s][(%s) %s]]" entry (format-time-string "%Y-%m-%d" (org-publish-find-date entry project)) (org-publish-find-title entry project))) ((eq style 'tree) ;; Return only last subdir. (file-name-nondirectory (directory-file-name entry))) (t entry)))

RSS

A simple RSS feed is also created using ox-rss. It includes the first section of the document as feed content (extracted using the ™/org-get-first-paragraph function). The RSS feed is also limited to the latest 30 entries.

There are currently issues with the styling and images in the feed, but it is sufficient for my simple use. A few tricks are worth explaining:

  • Set the PERMALINK field to link to articles. Since ox-rss prefixes the permalink with the URL of index.html, the constructed permalink starts with ../ to move up one level in the folder structure.
  • Set the publication date to the post date.
[my-website.el]
(defun ™/org-get-first-paragraph (file) "Get string content of first paragraph of file." (with-temp-buffer (insert-file-contents file) (goto-char (point-min)) (show-all) (let ((first-begin (progn (org-forward-heading-same-level 1) (point))) (first-end (progn (org-next-visible-heading 1) (point)))) (buffer-substring first-begin first-end)))) (defun ™/org-rss-publish-to-rss (plist filename pub-dir) "Prepare rss.org file before exporting." (let* ((basedir (plist-get plist :base-directory)) (postsdir (concat (file-name-as-directory basedir) "posts"))) (with-current-buffer (find-file filename) (erase-buffer) (insert "#+TITLE: misc/ blog RSS\n") (insert "#+SETUPFILE: setup.org\n") (insert "#+INCLUDE: source-blocks.org\n") (insert "#+OPTIONS: toc:nil\n") ;; The following does not seem to work (insert (concat "#+HTML_HEAD: <link rel=\"stylesheet\" type=\"text/css\"" " href=\"/blog/css/site.css\" />\n\n")) (let* ((files-all (reverse (directory-files postsdir nil "[0-9-]+.*\\.org$"))) (files (subseq files-all 0 (min (length files-all) 30)))) (dolist (post files) (let* ((post-file (concat (file-name-as-directory postsdir) post)) (post-title (org-publish-find-title post-file plist)) (preview-str (™/org-get-first-paragraph post-file)) (date (replace-regexp-in-string "\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\)-.*" "\\1" post))) (insert (concat "* [[file:posts/" post "][" post-title "]]\n\n")) (org-set-property "ID" post) ;; ox-rss prepends html-link-home to permalink (org-set-property "RSS_PERMALINK" (concat "../posts/" (file-name-sans-extension post) ".html")) (org-set-property "PUBDATE" (format-time-string "<%Y-%m-%d %a %H:%M>" (org-time-string-to-time (replace-regexp-in-string "\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\)-.*" "\\1" post)))) (insert "*") (insert preview-str) (newline 1) (insert (concat "[[file:posts/" post "][(Read more)]]\n\n")))) (save-buffer)))) (let ((user-mail-address "t") (org-export-with-broken-links t) (org-rss-use-entry-url-as-guid nil)) (org-rss-publish-to-rss plist filename pub-dir)))

Mathjax path

In order to use the website on secured connections (https), the https version of the mathjax library is loaded.

[my-website.el]
;; Use https for mathjax (setcdr (assq 'path org-html-mathjax-options) '("https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML"))

Custom backend

The backend used for exporting org files to HTML is derived from the builtin html backend, adding one functionality: the name of the tangle file for each source block is shown in the block, as an overlay.

To achieve this, the tangle process must be first initiated, i.e. tangled blocks must be collected using the org-babel-tangle-collect-blocks storing the tangled filename for each block in a cache (the ™/org-tangle-list-cache variable). Then, during export, the source blocks are filtered and the filename is added to the source block <pre> tag. Note that this is not valid CSS, but it seems to work on most browsers, so it will do for now.

Preprocess buffer

The ™/org-export-collect-tangle function is responsible for collecting source blocks for tangling and saving the output filename in the cache.

[my-website.el]
(defvar ™/org-tangle-list-cache nil) (defun ™/org-export-collect-tangle (backend) ;; Remove hook to ensure it does not get called recursively (remove-hook 'org-export-before-processing-hook '™/org-export-collect-tangle) (when (equal backend '™/html) (setq ™/org-tangle-list-cache nil) (save-excursion (save-restriction (widen) (show-all) (let ((src-blocks (org-babel-tangle-collect-blocks))) (dolist (src-lang src-blocks) (dolist (src-block (cdr src-lang)) (let ((src-block-name (nth 3 src-block)) (src-block-tangle (cdr (assoc :tangle (nth 4 src-block))))) (add-to-list '™/org-tangle-list-cache `(,src-block-name . ,src-block-tangle))))))))))

Source block processing

The derived backend modifies the source block handling function by inserting the name of the tangled file. The regular html exporter is first used to produce the html code for the source block (including syntax highlighting). The html output is then modified by inserting a <div> block, fixed at the top right corner of the source block. The CSS class src-label is added to the div block to allow for customization of the appearance.

[my-website.el]
(defun ™/html-src-block (src-block contents info) "Transcode a SRC-BLOCK element from Org to HTML. CONTENTS is nil. INFO is a plist used as a communication channel. Add name of tangle file to source block (this requires a preprocessing hook to be added with `™/org-export-collect-tangle')." (let* ((src-block-name (org-element-property :name src-block)) (src-block-tangle (when ™/org-tangle-list-cache (cdr (assoc src-block-name ™/org-tangle-list-cache)))) (export-out (org-export-with-backend 'html src-block contents info))) (when (and src-block-tangle (> (length src-block-tangle) 0) (not (string= src-block-tangle "no"))) (let ((src-start-pat "\\(<pre class=\"src src-[^>]+>\\)")) (setq export-out (replace-regexp-in-string src-start-pat (concat "\\1<div style=\"position: absolute; top: 0;" " right: 0; text-align:right;\" class=\"src-label\">[" src-block-tangle "]</div>") export-out)))) export-out))

Finally, the derived backend is defined, using the custom translation function for source blocks. Everything else is handled by the regular html backend.

[my-website.el]
(org-export-define-derived-backend '™/html 'html :translate-alist '((src-block . ™/html-src-block)))

Publishing function

A simple wrapper function is created to export to the custom backend. It is almost identical to org-html-publish-to-html with the exception of the actual backend used.

[my-website.el]
(defun ™/org-html-publish-to-™-html (plist filename pub-dir) "Publish an org file to HTML. FILENAME is the filename of the Org file to be published. PLIST is the property list for the given project. PUB-DIR is the publishing directory. Return output file name." (add-hook 'org-export-before-processing-hook '™/org-export-collect-tangle) (let ((res (org-publish-org-to '™/html filename (concat "." (or (plist-get plist :html-extension) org-html-extension "html")) plist pub-dir))) (remove-hook 'org-export-before-processing-hook '™/org-export-collect-tangle) res))

Org-publishing setup

The publishing configuration is set via the org-publish-project-alist variable. The following function returns a list of project properties to use with org-publish-project-alist.

It relies on the ™/get-perso utility to get the root folder (line 5). This can be replaced by a full path to the website root folder.

The function defines the paths to the org source, the destination, the attachment folders, etc. and loads the content of the custom HTML head, preamble and postamble. It creates multiple projects to control the publishing of the website:

  • w-org is the main publishing of org files to html. It uses two publishing-function entries: one to generate the html file and one to export a complete org file (which can be accessed on the output webpage by clicking on the buttons defined in the footer). The sitemap generation is also configured in this project. Additional features include the insertion of a RSS link to the header, and the suppression of TODO items and statistics cookies.
  • w-images simply publishes images in the images/ folder using the org-publish-attachment function.
  • w-js publishes the javascript files in the js/ folder.
  • w-css publishes the CSS file in the css/ folder.
  • w-rss generates the RSS feed using the ox-rss package.
  • Finally, website is a meta project which simply encapsulates all the other projects.
[my-website.el]
;; Get project settings (defun ™/get-publish-project-spec () "Return project settings for use with `org-publish-project-alist'." (let* ((website-root (file-name-as-directory (™/get-perso "website-root"))) (website-org (file-name-as-directory (concat website-root "org"))) (website-www (file-name-as-directory (concat website-root "www"))) (website-org-img (file-name-as-directory (concat website-org "images"))) (website-www-img (file-name-as-directory (concat website-www "images"))) (website-org-js (file-name-as-directory (concat website-org "js"))) (website-www-js (file-name-as-directory (concat website-www "js"))) (website-org-css (file-name-as-directory (concat website-org "css"))) (website-www-css (file-name-as-directory (concat website-www "css"))) (website-org-html (file-name-as-directory (concat website-org "html"))) (website-org-posts (file-name-as-directory (concat website-org "posts"))) (get-content (lambda (x) (with-temp-buffer (insert-file-contents (concat website-org-html x)) (buffer-string)))) (website-html-head (funcall get-content "html_head.html")) (website-html-preamble (funcall get-content "html_preamble.html")) (website-html-postamble (funcall get-content "html_postamble.html"))) `( ("w-org" :base-directory ,website-org :base-extension "org" :recursive t :exclude "setup\\.org\\|website\\.org\\|rss\\.org\\|source-blocks\\.org" :publishing-directory ,website-www :publishing-function (™/org-html-publish-to-™-html org-org-publish-to-org) :html-extension "html" :htmlized-source t :html-doctype "html5" ;;:html-html5-fancy t :language "en" :section-numbers nil :auto-sitemap t :sitemap-date-format "Published: %a %b %d %Y" :sitemap-sort-files anti-chronologically :sitemap-function ™/org-publish-org-sitemap :sitemap-format-entry ™/org-publish-org-sitemap-format :with-toc nil ;; except when otherwise stated :with-date t :headline-levels 4 :with-sub-superscript t :with-todo-keywords nil :with-statistics-cookies nil :with-email nil :html-link-home "/blog/index.html" :html-link-up "/blog/index.html" :html-head-extra "<link rel=\"alternate\" type=\"application/rss+xml\" href=\"/blog/rss.xml\" title=\"RSS feed\">" :html-head ,website-html-head :html-preamble ,website-html-preamble :html-postamble ,website-html-postamble) ("w-images" :base-directory ,website-org-img :base-extension "jpg\\|gif\\|png" :recursive t :publishing-directory ,website-www-img :publishing-function org-publish-attachment) ("w-js" :base-directory ,website-org-js :base-extension "js" :publishing-directory ,website-www-js :publishing-function org-publish-attachment) ("w-css" :base-directory ,website-org-css :base-extension "css" :publishing-directory ,website-www-css :publishing-function org-publish-attachment) ("w-rss" :base-directory ,website-org :base-extension "org" :include ("rss.org") :exclude ".*" :with-author nil :with-todo-keywords nil :with-statistics-cookies nil :with-broken-link t :with-email nil :html-link-home "/blog/index.html" :publishing-directory ,website-www :publishing-function ™/org-rss-publish-to-rss :section-numbers nil :html-link-use-abs-url t) ("website" :components ("w-org" "w-images" "w-js" "w-css" "w-rss")))))

Publishing wrapper function

The ™/publish-website function is the entry point to generate the website from the source org files. It sets up the configuration lists, defines some simple options and calls the publishing function.

The website is updated when calling (™/publish-website), which only publishes newly modified files. When used with additional arguments, a full update can be forced: (™/publish-website "website" t).

[my-website.el]
;; org-publish for website (defun ™/publish-website (&optional project force) "Publish personal website." (interactive) (unless project (setq project "website")) (let ((org-publish-project-alist (™/get-publish-project-spec)) (org-export-date-timestamp-format "%Y-%m-%d") (org-todo-keywords '((sequence "TODO" "REVIEW" "|" "DONE" "DEFERRED" "ABANDONED")))) (org-publish-project project force)))

Push org-file to blog

The mechanism to publish new posts uses the ™/push-to-blog function to convert a standalone org file into a post. The function performs the following:

  • add a #+SETUPFILE line to the header (the content of the setup file is described here),
  • format the output filename with the post date preceding the title (e.g. YYYY-MM-DD-Post_title.org),
  • search for TODO items, exit if any is found,
  • move file links (images) to a subfolder of the org/images/ folder named after the formatted output filename (e.g. YYYY-MM-DD-Post_title/),
  • modify links to files to point to the copy location,
  • finally, save the modified org file to the org/posts/ folder.
[my-website.el]
;; Push to blog (defun ™/push-to-blog (org-file) "Add ORG-FILE to personal blog." (let* ((blog-dir (concat (file-name-as-directory (™/get-perso "website-root")) (file-name-as-directory "org"))) (post-dir (concat (file-name-as-directory blog-dir) (file-name-as-directory "posts"))) (img-dir-base (concat (file-name-as-directory blog-dir) (file-name-as-directory "images"))) (file-base (file-name-base org-file)) post-date (has-date nil) out-file (org-inhibit-startup t) (visiting (find-buffer-visiting org-file)) (buffer (or visiting (find-file-noselect org-file))) (org-tree (with-current-buffer buffer (org-element-parse-buffer)))) (let (setupfile-added-p) ;; Add SETUPFILE entry to tree, parse TITLE and DATE (org-element-map org-tree 'keyword (lambda (r) (let ((key (org-element-property :key r)) (value (org-element-property :value r))) (unless setupfile-added-p (let ((elt-setup (org-element-copy r)) (elt-src-block (org-element-copy r))) (org-element-put-property elt-src-block :key "INCLUDE") (org-element-put-property elt-src-block :value "../source-blocks.org") (org-element-insert-before elt-src-block r) (org-element-put-property elt-setup :key "SETUPFILE") (org-element-put-property elt-setup :value "../setup.org") (org-element-insert-before elt-setup r)) (setq setupfile-added-p t)) (cond ((string= key "TITLE") (setq out-file (replace-regexp-in-string "[[:space:]]+" "_" value))) ((string= key "DATE") (setq post-date (format-time-string "%Y-%m-%d" (org-time-string-to-time value)))))) (and out-file post-date setupfile-added-p)) nil t) (when (not (and post-date out-file setupfile-added-p)) (error "TITLE and DATE must be defined"))) ;; Look for remaining TODO (let ((has-todo nil)) (org-element-map org-tree 'headline (lambda (r) (setq has-todo (or has-todo (equal 'todo (org-element-property :todo-type r)))) has-todo) nil t) (when has-todo (error "Remaining TODO items in file"))) (let* ((base-fname (concat post-date "-" out-file)) (out-file-full (concat post-dir base-fname ".org")) (img-dir (concat img-dir-base base-fname))) ;; Copy images and update links (org-element-map org-tree 'link (lambda (r) (when (string= (org-element-property :type r) "file") (let* ((link-file (org-element-property :path r)) (link-file-base (file-name-base link-file)) (link-file-ext (file-name-extension link-file t)) (out-link (concat (file-name-as-directory img-dir) link-file-base link-file-ext)) (img-path (concat "../images/" (file-name-as-directory base-fname) link-file-base link-file-ext))) (when (member link-file-ext '(".png" ".jpg" ".tif")) ;; Copy file (mkdir img-dir t) (copy-file link-file out-link t) ;; Update link target (org-element-put-property r :path img-path) (setcar (org-element-contents r) (concat "file:" img-path)) (setcdr (org-element-contents r) nil)))))) ;; Write output (with-current-buffer (find-file out-file-full) (erase-buffer) (insert (org-element-interpret-data org-tree)) (save-buffer)))))

Footer

This block closes the my-website.el file.

[my-website.el]
(provide 'my-website) ;;; my-website.el ends here