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:
- http://bastibe.de/2013-11-13-blogging-with-emacs.html
- http://emacs-doctor.com/blogging-from-emacs.html
- http://endlessparentheses.com/how-i-blog-one-year-of-posts-in-a-single-org-file.html
- https://github.com/howardabrams/dot-files/blob/master/emacs-blog.org
- http://www.john2x.com/blog/blogging-with-orgmode.html
- https://ogbe.net/blog/blogging_with_org.html
- http://nicolas.petton.fr/blog/blogging-with-org-mode.html
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.
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:
- the CSS file controlling the website appearance,
- some javascript code used by the HTML postamble buttons to link to the org file source (and a htmlized version of it).
[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 oforg-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 " ") " ")) #+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 ofindex.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 twopublishing-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 theimages/
folder using theorg-publish-attachment
function.w-js
publishes the javascript files in thejs/
folder.w-css
publishes the CSS file in thecss/
folder.w-rss
generates the RSS feed using theox-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