[index] [pictures] [about] [how]

How the website is built

Table of Contents

Introduction

The website is built with Org Mode in Emacs. The code blocks in this file are extracted with org-babel-tangle and written to an ELisp file. This file is then run in emacs batch mode to export the org files to HTML and to create the RSS feed.

Initialization

Initialize the package system:

;;; build.el -*- lexical-binding: t; -*-
(require 'package)
(setq package-user-dir (expand-file-name "./.packages"))
(setq package-archives '(("melpa" . "https://melpa.org/packages/")
                         ("elpa" . "https://elpa.gnu.org/packages/")))

;; Initialize the package system
(package-initialize)
(unless package-archive-contents
  (package-refresh-contents))

Install and load dependencies:

(package-install 'htmlize)
(package-install 'org-ref)
(package-install 'ox-rss)

(require 'org-ref)
(require 'ox-html)
(require 'ox-rss)
(require 'ox-publish)
(require 'org-inlinetask)

Definitions

(defvar relint/title "relint")
(defvar relint/home-url "https://relint.de")
(defvar relint/author-email "")

Some files should not be exported as HTML. This is defined in the first of the following three functions. Some files should be exported, but not appear in the sitemap. This is defined by the files in the second function. And finally some files should not appear in the RSS feed. This is what the last function is for.

(defun relint/publish-html-excludes-regexp ()
  ;; (regexp-opt '("index.org" "ideas.org" "rss.org"))
  "\\(?:\\(?:ideas\\|index\\|rss\\|pictures-rss\\)\\.org\\)\\|\\(\\.draft\\.org\\)")

(defun relint/publish-sitemap-excludes-regexp ()
  (regexp-opt '("about.org")))

(defun relint/publish-rss-excludes-regexp ()
  ;; (regexp-opt '("index.org" "about.org" "rss.org" "ideas.org"))
  "\\(?:\\(?:ideas\\|index\\|rss\\|about\\|pictures\\|pictures-rss\\)\\.org\\)\\|\\(\\.draft\\.org\\)")

Here are some templates that get used in the HTML head and the header and the footer of the page:

(defun relint/icons-head ()
  (concat "<link rel=\"shortcut icon\" href=\"/assets/img/favicon.ico\">\n"
          "<link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/assets/img/apple-touch-icon.png\">\n"
          "<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/assets/img/favicon-32x32.png\">\n"
          "<link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/assets/img/favicon-16x16.png\">\n"
          "<link rel=\"manifest\" href=\"/assets/img/site.webmanifest\">\n"))

(defun relint/link-js (js-file)
  (format "<script type=\"text/javascript\" defer src=\"%s\"></script>\n" js-file))

(defun relint/link-css (css-file)
  (format "<link rel=\"stylesheet\" type=\"text/css\" defer href=\"%s\" />\n" css-file))

(defun relint/preamble (plist)
  "<span>
    <a href=\"/\">[index]</a>
    <a href=\"/pictures.html\">[pictures]</a>
    <a href=\"/about.html\">[about]</a>
    <a href=\"/build.html\">[how]</a>
    <label id=\"theme-switch\" for=\"checkbox-theme\">Dark mode: <input type=\"checkbox\" id=\"checkbox-theme\"></label>
    </span>
    <div id=\"lightbox-outer\">
      <div id=\"lightbox-inner\">
        <img id=\"lightbox-image\" src=\"\" alt=\"\" />
      </div>
      <div id=\"lightbox-close\">[x]</div>
    </div>")

(defun relint/postamble (info)
  (and (plist-get info :time-stamp-file)
       (format
        "<span>RSS:
           <a href=\"/rss.xml\">[index]</a>
           <a href=\"/pictures-rss.xml\">[pictures]</a>
         </span>
         <span id=\"build\">%s: %s</span>"
        (org-html--translate "Last build" info)
        (format-time-string
         (plist-get info :html-metadata-timestamp-format)))))

(setq org-html-footnotes-section "<div id=\"footnotes\">
    <h2 class=\"footnotes\">%s</h2>
    <div id=\"text-footnotes\">
    %s
    </div>
    </div>")

The list of sites on the frontpage is generated with the sitemap functionality. For each page we just generate a link to that page with the date in parentheses. We then print them as an unordered list:

(defun relint/org-publish-sitemap-entry (entry style project)
  ;; (unless (string-match-p (regexp-opt '("about.org")) entry)
  (format "[[file:%s][%s]] (%s)"
          entry
          (org-publish-find-title entry project)
          (format-time-string "%Y-%m-%d" (org-publish-find-date entry project))))

(defun relint/org-publish-sitemap (title list)
  (let ((filtered-list (cl-remove-if
                        (lambda (el)
                          (string-match (relint/publish-sitemap-excludes-regexp) (car el)))
                        (cdr list))))
    (concat "#+TITLE: " title "\n\n"
            "There is not a lot of content yet.\n"
            (org-list-to-org (cons 'unordered filtered-list)))))

The RSS is also generated by the sitemap functionality. For each page we create a top level heading with the RSS_PERMALINK and PUBDATE properties. These are processed later by ox-rss.

(defun relint/org-publish-rss-feed-entry (entry style project)
  (let* ((file (org-publish--expand-file-name entry project))
         (title (org-publish-find-title entry project))
         (date (format-time-string "%Y-%m-%d" (org-publish-find-date entry project)))
         (descr (org-publish-find-property entry :description project 'html))
         (link (concat (file-name-sans-extension entry) ".html")))
    (with-temp-buffer
      ;; We have to be in org-mode for org-set-property to work
      (org-mode)
      (insert (format "* %s\n" title))
      (org-set-property "RSS_PERMALINK" link)
      (org-set-property "PUBDATE" date)
      (if (char-or-string-p descr)
          (insert descr))
      (buffer-string))))

(defun relint/org-publish-rss-feed (title list)
  (concat "#+TITLE: " title "\n\n"
          (org-list-to-subtree list nil '(:icount "" :istart ""))))

(defun relint/org-publish-rss (plist filename pub-dir)
  (if (or (equal "rss.org" (file-name-nondirectory filename))
          (equal "pictures-rss.org" (file-name-nondirectory filename)))
      (org-rss-publish-to-rss plist filename pub-dir)))

In the following list we define what is actually exported. Some static assets like JavaScript, CSS files and images are exported with org-publish-attachment. The main content is exported with org-html-publish-to-html. For the RSS feed we use the relint/org-publish-rss function above that process the sitemap written to a special file.

(defvar relint/web-assets
  (list "web-assets"
        :recursive t
        :base-directory "./assets"
        :base-extension "css\\|js\\|png\\|jpg\\|gif\\|svg\\|pdf\\|woff2\\|ico\\|webmanifest\\|wasm\\|glsl"
        :publishing-directory "./public/assets"
        :publishing-function 'org-publish-attachment))

(defvar relint/web-content
  (list "web-content"
        :recursive nil
        :exclude (relint/publish-html-excludes-regexp)
        :base-extension "org"
        :base-directory "./content"
        :publishing-directory "./public"
        :publishing-function 'org-html-publish-to-html
        :section-numbers nil
        :html-doctype "html5"
        :html-preamble 'relint/preamble
        :html-postamble 'relint/postamble
        :html-metadata-timestamp-format "%Y-%m-%d %H:%M:%S"
        :htmlized-source t
        :html-head-extra (concat (relint/icons-head)
                                 (relint/link-css "assets/css/syntax.css")
                                 (relint/link-css "assets/css/main.css")
                                 (relint/link-js "assets/js/utils.js"))
        :auto-sitemap t
        :sitemap-filename "index.org"
        :sitemap-title relint/title
        :sitemap-style 'list
        :sitemap-format-entry 'relint/org-publish-sitemap-entry
        :sitemap-function 'relint/org-publish-sitemap
        :sitemap-sort-files 'anti-chronologically))

(defvar relint/web-rss
  (list "web-rss"
        :recursive nil
        :exclude (relint/publish-rss-excludes-regexp)
        :base-extension "org"
        :base-directory "./content"
        :publishing-directory "./public"
        :publishing-function 'relint/org-publish-rss
        :rss-extension "xml"
        :html-link-home relint/home-url
        :html-link-use-abs-url t
        :html-link-org-files-as-html t
        :email relint/author-email
        :author ""
        :auto-sitemap t
        :sitemap-filename "rss.org"
        :sitemap-title relint/title
        :sitemap-style 'list
        :sitemap-format-entry 'relint/org-publish-rss-feed-entry
        :sitemap-function 'relint/org-publish-rss-feed
        :sitemap-sort-files 'anti-chronologically))

(defvar relint/web-pictures
  (list "web-pictures"
        :recursive nil
        :exclude (relint/publish-html-excludes-regexp)
        :base-extension "org"
        :base-directory "./content/pictures"
        :publishing-directory "./public"
        :publishing-function 'org-html-publish-to-html
        :section-numbers nil
        :html-doctype "html5"
        :html-preamble 'relint/preamble
        :html-postamble 'relint/postamble
        :html-metadata-timestamp-format "%Y-%m-%d %H:%M:%S"
        :htmlized-source t
        :html-head-extra (concat (relint/icons-head)
                                 (relint/link-css "assets/css/syntax.css")
                                 (relint/link-css "assets/css/main.css")
                                 (relint/link-js "assets/js/utils.js"))
        :auto-sitemap t
        :sitemap-filename "pictures.org"
        :sitemap-title relint/title
        :sitemap-style 'list
        :sitemap-format-entry 'relint/org-publish-sitemap-entry
        :sitemap-function 'relint/org-publish-sitemap
        :sitemap-sort-files 'anti-chronologically))

(defvar relint/web-pictures-rss
  (list "web-pictures"
        :recursive nil
        :exclude (relint/publish-rss-excludes-regexp)
        :base-extension "org"
        :base-directory "./content/pictures"
        :publishing-directory "./public"
        :publishing-function 'relint/org-publish-rss
        :rss-extension "xml"
        :html-link-home relint/home-url
        :html-link-use-abs-url t
        :html-link-org-files-as-html t
        :email relint/author-email
        :author ""
        :auto-sitemap t
        :sitemap-filename "pictures-rss.org"
        :sitemap-title relint/title
        :sitemap-style 'list
        :sitemap-format-entry 'relint/org-publish-rss-feed-entry
        :sitemap-function 'relint/org-publish-rss-feed
        :sitemap-sort-files 'anti-chronologically))
(setq org-publish-project-alist
      (list
       relint/web-assets
       relint/web-content
       relint/web-rss
       relint/web-pictures
       relint/web-pictures-rss))

(setq org-html-validation-link nil
      org-html-htmlize-output-type "css"
      org-html-head-include-scripts nil
      org-html-head-include-default-style nil)

(setq org-confirm-babel-evaluate nil)
(setq org-rss-use-entry-url-as-guid t)

(org-publish-all t)

(message "Done.")
RSS: [index] [pictures] Last build: 2025-01-05 16:27:21