Created: February 9, 2019

Last modified: January 31, 2026

Configuration for this website

This is the org-publish configuration for this website. The css styling lives here. The features this website supports can be viewed here. See How I publish my wiki with org-publish for another take on exporting org-roam using org-publish.

HTML headers

<link href="/favicon.svg" rel="icon" type="image/svg+xml">
<link href="/font/fira.css" rel="stylesheet" type="text/css">
<link href="/styles.css" rel="stylesheet" type="text/css">
<script src="/mathjax-init.js" type="text/javascript" async></script>
<script src="/load-file.js" type="text/javascript" async></script>
<link href="/reveal.css" rel="stylesheet" type="text/css">
<script src="/revealjs/reveal.js" type="module" defer></script>
<script src="/revealjs/notes/plugin.js" type="module" defer></script>
<script src="/reveal-init.js" type="module" defer></script>
<div class="menu">
<a class="menu-btn" id="menu-home" href="/">Akira Kyle</a>
<span style="color: var(--func);">|</span>
<a class="menu-btn" id="menu-papers" href="/papers">papers</a>
<span style="color: var(--func);">|</span>
<a class="menu-btn" id="menu-talks" href="/talks">talks</a>
<span style="color: var(--func);">|</span>
<a class="menu-btn" id="menu-blog" href="/blog">interoam</a>
</div>
(let* ((named-element (org-element-map (org-element-parse-buffer) org-element-all-elements
                        (lambda (element)
                          (when (string= (org-element-property :name element) name)
                            element))
                        nil t))
       (result (org-element-property :value named-element)))
  (format "\"%s\"" (replace-regexp-in-string "\\\"" "\\\\\"" result))) ;; escape quote
(setq my-org-html-head
<<blk-to-elisp-str("html-head")>>
)
(setq my-org-html-head-reveal
<<blk-to-elisp-str("html-head-reveal")>>
)
(setq my-org-html-preamble
<<blk-to-elisp-str("html-preamble")>>
)

A bunch of emacs-lisp helper functions

List of notes/pages

(defun my-org-get-env-key (file key)
  "Extract the value of keywords on beginning lines starting with `#+:` from `file`"
  (with-temp-buffer (insert-file-contents file)
                    (org-mode)
                    (cadar (org-collect-keywords (list key)))))

(defun my-note-filenames ()
  "Returns a list of all files sorted approprately."
  (seq-filter
   (lambda (fname)
     (not (my-org-get-env-key fname "hide")))
   (mapcan (lambda (dir)
             (directory-files
              (file-name-as-directory (concat my-org-dir dir)) t ".*\\.org$" nil))
           '("notes" "config"))))

(defun my-sort-note-filenames (fnames)
  "Returns a list of all files sorted approprately."
  (sort fnames (lambda (a b)
                 (>
                  (org-time-string-to-seconds (my-org-get-env-key a "date"))
                  (org-time-string-to-seconds (my-org-get-env-key b "date"))))))

(defun my-filenames-to-org-list ()
  (seq-reduce
   (lambda (acc el) (concat acc "\n- " el))
   (seq-map (lambda (fname)
              (concat
               (org-export-get-date
                `(:date ,(org-element-parse-secondary-string
                          (my-org-get-env-key fname "date")
                          (org-element-restriction 'keyword))) "~%Y-%m-%d~")
               ": "
               (org-link-make-string
                (concat "file:" (file-relative-name fname))
                (my-org-get-env-key fname "title"))))
            (my-sort-note-filenames (my-note-filenames)))
   ""))

org roam export backlinks and reflinks and citations

  • insert-bibliography eliminates the need for #+print_bibliography: at the end of every file
    • my-filter-citations makes org citations link to the corresponding org-roam nodes
      • I didn’t end up trying to use the csl formatter to get the nice, configurable inline style, but rather just make the org-roam title close to what the csl formatter would do (yeah this is hacky).
;; TODO: Still need to fix spacing also consider leaving in original citation, just add link to ref-notes only if it exists...
(defun my-filter-citations (data backend info)
  (let* ((cite-to-id
          (lambda (el)
            (let* ((key (concat "@" (org-element-property :key el)))
                   (node (org-roam-node-from-ref key))
                   (_ (and (not node) (error "No org roam node for key %s" key)))
                   (id (org-roam-node-id node))
                   (title (org-roam-node-title node)))
              (org-link-make-string (concat "id:" id) title))))
;                   (id (if node
;                           (concat "id:" (org-roam-node-id node))
;                         (message "No org roam node for key %s" key)
;                         "id:C2668D73-1846-4A59-82DC-76B911AAE86B"))
;                   (title (if node
;                              (org-roam-node-title node)
;                            key)))
;              (org-link-make-string id title))))
         (cites-to-ids
          (lambda (elts)
            (with-temp-buffer
              (save-excursion
                (insert (concat " (" (mapconcat cite-to-id elts "; ") ") "))
                (car (org-element-map (org-element-parse-buffer) 'paragraph
                       'org-element-contents))))))
         (map-citation
          (lambda (el)
            (unless (org-element-property :style el)
              (mapc (lambda (l) (org-element-insert-before l el))
                    (funcall cites-to-ids (org-element-contents el)))
              (org-element-extract-element el)))))
    (org-element-map data 'citation map-citation info nil nil t)
    data))

(defun insert-bibliography (backend)
  "Insert reflinks into the end of the org file before parsing it."
  (when (eq backend 'html)
    (goto-char (point-min))
    (when (re-search-forward "\\[cite:.*\\]" nil t)
      (goto-char (point-max))
      (insert "\n\n* references\n")
      (insert "#+print_bibliography: :title \"references\""))))

;; https://org-roam.discourse.group/t/export-backlinks-on-org-export/1756/2?page=2
(defun collect-backlinks-string (backend)
  "Insert backlinks into the end of the org file before parsing it."
  (when (and (or (eq backend 'html) (eq backend 'html-with-cite-filter))
             (org-roam-node-at-point))
    (let* ((backlinks (org-roam-backlinks-get (org-roam-node-at-point))))
      (when backlinks
        (goto-char (point-max))
        (insert "\n\n* backlinks\n")
        (dolist (backlink backlinks)
          (let* ((source-node (org-roam-backlink-source-node backlink))
                 (point (org-roam-backlink-point backlink)))
            (insert
             (format "- [[id:%s][%s]]\n"
                     (file-name-nondirectory (org-roam-node-id source-node))
                     (org-roam-node-title source-node)))))))))

(defun collect-reflinks-string (backend)
  "Insert reflinks into the end of the org file before parsing it."
  (when (and (or (eq backend 'html) (eq backend 'html-with-cite-filter))
             (org-roam-node-at-point))
    (let* ((reflinks (org-roam-reflinks-get (org-roam-node-at-point))))
      (when reflinks
        (goto-char (point-max))
        (insert "\n\n* reflinks:\n")
        (dolist (reflink reflinks)
          (let* ((source-node (org-roam-reflink-source-node reflink))
                 (point (org-roam-reflink-point reflink)))
            (unless (string= (org-roam-node-id source-node)
                             (org-roam-node-id (org-roam-node-at-point)))
              (insert
               (format "- [[id:%s][%s]]\n"
                       (file-name-nondirectory (org-roam-node-id source-node))
                       (org-roam-node-title source-node))))))))))

remove automatically generated ids

(defun unpackaged/org-export-new-title-reference (datum cache)
  "Return new reference for DATUM that is unique in CACHE."
  (cl-macrolet ((inc-suffixf (place)
                             `(progn
                                (string-match (rx bos
                                                  (minimal-match (group (1+ anything)))
                                                  (optional "--" (group (1+ digit)))
                                                  eos)
                                              ,place)
                                ;; HACK: `s1' instead of a gensym.
                                (-let* (((s1 suffix) (list (match-string 1 ,place)
                                                           (match-string 2 ,place)))
                                        (suffix (if suffix
                                                    (string-to-number suffix)
                                                  0)))
                                  (setf ,place (format "%s--%s" s1 (cl-incf suffix)))))))
    (let* ((title-raw (org-element-property :title datum))
           (title (org-export-string-as (org-element-interpret-data title-raw) 'org t))
           (ref (replace-regexp-in-string "[^a-zA-Z0-9]+" "" title))
           (parent (org-element-property :title datum)))
      (while (--any (equal ref (car it))
                    cache)
        ;; Title not unique: make it so.
        (if parent
            ;; Append ancestor title.
            (setf title (concat (org-element-property :title parent) "--" title)
                  ;ref (url-hexify-string (substring-no-properties title))
                  ref (replace-regexp-in-string "[^a-zA-Z0-9]+" "-" (substring-no-properties title))
                  parent (org-element-property :parent parent))
          ;; No more ancestors: add and increment a number.
          (inc-suffixf ref)))
      ref)))

(defun unpackaged/org-export-get-reference (datum info)
  "Like `org-export-get-reference', except uses heading titles instead of random numbers."
  (let ((cache (plist-get info :internal-references)))
    (or (car (rassq datum cache))
        (let* ((crossrefs (plist-get info :crossrefs))
               (cells (org-export-search-cells datum))
               ;; Preserve any pre-existing association between
               ;; a search cell and a reference, i.e., when some
               ;; previously published document referenced a location
               ;; within current file (see
               ;; `org-publish-resolve-external-link').
               ;;
               ;; However, there is no guarantee that search cells are
               ;; unique, e.g., there might be duplicate custom ID or
               ;; two headings with the same title in the file.
               ;;
               ;; As a consequence, before re-using any reference to
               ;; an element or object, we check that it doesn't refer
               ;; to a previous element or object.
               (new (or (cl-some
                         (lambda (cell)
                           (let ((stored (cdr (assoc cell crossrefs))))
                             (when stored
                               (let ((old stored))
                                 (and (not (assoc old cache)) stored)))))
                         cells)
                        (when (org-element-property :raw-value datum)
                          ;; Heading with a title
                          (unpackaged/org-export-new-title-reference datum cache))
                          ;; NOTE: This probably breaks some Org Export
                          ;; feature, but if it does what I need, fine.
                          (org-export-format-reference
                           (org-export-new-reference cache))))
               (reference-string new))
          ;; Cache contains both data already associated to
          ;; a reference and in-use internal references, so as to make
          ;; unique references.
          (dolist (cell cells) (push (cons cell new) cache))
          ;; Retain a direct association between reference string and
          ;; DATUM since (1) not every object or element can be given
          ;; a search cell (2) it permits quick lookup.
          (push (cons reference-string datum) cache)
          (plist-put info :internal-references cache)
          reference-string))))

(advice-add #'org-export-get-reference :override #'unpackaged/org-export-get-reference)
;(advice-remove #'org-export-get-reference #'unpackaged/org-export-get-reference)

org-publish setup

;; I use the ~#+modified:~ property rather than org's default of using the
;; file's modification time, since my org files are in git
(defun my-org-html-build-preamble (info)
  (let* ((spec (org-html-format-spec info))
         (date (cdr (assq ?d spec)))
         (timestamp-format (plist-get info :html-metadata-timestamp-format))
         (modified-str (cadar (org-collect-keywords '("modified"))))
         (modified-parsed (org-element-parse-secondary-string
                           modified-str (org-element-restriction 'keyword)))
         (modified (org-export-get-date `(:date ,modified-parsed)
                                        timestamp-format)))
    (concat
     my-org-html-preamble
     (and (plist-get info :with-date)
          (org-string-nw-p date)
          (format "<p class=\"date\">Created: %s</p>\n" date))
     (and (plist-get info :with-date)
          (org-string-nw-p modified)
          (format "<p class=\"date\">Last modified: %s</p>\n" modified)))))

;;(setq org-export-async-init-file (expand-file-name "~/notebook/setup.el")
;;      org-export-in-background t)
(setq org-html-metadata-timestamp-format "%B %e, %Y")
(setq org-html-htmlize-output-type 'css)
(setq org-html-html5-fancy t)
(setq org-html-doctype "html5")
(setq org-export-with-toc nil)
(setq org-export-with-section-numbers nil)
(setq org-export-time-stamp-file nil)
(setq org-html-head-include-default-style nil)
(setq org-html-head-include-scripts nil)
(setq org-html-head my-org-html-head)
(setq org-html-preamble 'my-org-html-build-preamble)
(setq org-html-postamble nil)
(setq org-html-mathjax-template "")
(setq org-html-self-link-headlines t)
(setq org-html-prefer-user-labels t)

;; for org-html-link to omit .html from a href links
(setq org-html-extension "")

(org-export-define-derived-backend 'html-with-cite-filter 'html)

(defun my-org-html-publish-to-html (plist filename pub-dir)
  (org-publish-org-to 'html filename ".html" plist pub-dir))

(defun my-org-html-publish-to-html-with-cite-filter (plist filename pub-dir)
  (let ((org-export-filter-parse-tree-functions '(my-filter-citations)))
    (org-publish-org-to 'html-with-cite-filter filename ".html" plist pub-dir)))

(add-hook 'org-export-before-processing-functions 'insert-bibliography)
(add-hook 'org-export-before-processing-functions 'collect-backlinks-string)
(add-hook 'org-export-before-processing-functions 'collect-reflinks-string)

(defun my-publish-collab-dir (dir)
  `(,dir
    :base-directory ,(concat my-org-dir dir)
    :publishing-directory ,(concat my-website-publish-dir dir my-collab-url)
    :publishing-function my-org-html-publish-to-html-with-cite-filter))
    ;; :html-head-extra ,my-org-html-head-reveal

(defun my-publish-dir (dir)
  `(,dir
    :base-directory ,(concat my-org-dir dir)
    :publishing-directory ,(concat my-website-publish-dir dir)
    :publishing-function my-org-html-publish-to-html))
    ;;:html-postamble "Insert comments here..."))

(setq org-publish-project-alist
      `(
        ,(cons "top" (cdr (my-publish-dir "")))
        ,(my-publish-dir "config")
        ,(my-publish-dir "notes")
        ,(my-publish-dir "talks")
        ,(my-publish-collab-dir "notebook")
        ,(my-publish-collab-dir "present")
        ,(my-publish-collab-dir "ref-notes")
        ("website" :components ("top" "config" "notes" "talks"))
        ))
<<org-html-strings>>
<<org-dir-vars>>
<<org-html-export-remove-auto-id>>
<<list-org-files>>
<<org-roam-export-helpers>>
<<org-publish-project>>
(org-publish "website" t)

Need to run org-id-update-id-locations and org-roam-update-org-id-locations when exporting and get “org-export-data: Unable to resolve link”

dir locals

((org-mode . (
              (eval . (add-hook 'after-save-hook 'org-publish-current-file nil t))
              (eval . (setq-local org-html-head-extra my-org-html-head-reveal))
              ;(org-html-head-extra . my-org-html-head-reveal)
              ;(org-html-preamble . nil)
              ;(org-export-with-title . nil)
              )))
<<talk-dir-locals>>
<<talk-dir-locals>>

Mathjax JS

https://groups.google.com/g/mathjax-users/c/M6Uh4ANEdPU https://github.com/mathjax/MathJax/issues/3304 https://github.com/mathjax/MathJax/issues/3392

window.MathJax = {
    options: {
        menuOptions: {
            settings: {
                enrich: false,
            }
        },
    },
    loader: {
        load: ['[tex]/physics', '[tex]/mathtools']
    },
    tex: {
        packages: {'[+]': ['physics', 'mathtools']},
        tags: "ams",
        ams: {
            multlineWidth: "85%"
        },
        mathtools: {
            'use-unicode': true
        }
    },
    output: {
        font: 'mathjax-fira',
        fontPath: '/mathjax/fira-font'
    }
};
(function () {
    var script = document.createElement('script');
    script.src = '/mathjax/tex-chtml-nofont.js';
    script.async = true;
    document.head.appendChild(script);
})();

load-file js

Inspired by js from

customElements.define("load-file", class extends HTMLElement {
  async connectedCallback() {
    const shadowRoot = this.attachShadow({mode:"open"});
    shadowRoot.innerHTML = await (await fetch(this.getAttribute("src"))).text();

    this.querySelectorAll("param").forEach(function(p) {
        var svg_elem = shadowRoot.querySelector("#".concat(p.getAttribute("id")));
        for (const attr of p.attributes) {
            svg_elem.setAttribute(attr.name, attr.value);
        }
    });
    this.replaceWith(...shadowRoot.childNodes);
  }
})

Reveal JS

import Reveal from './revealjs/index.js'
import RevealNotes from './revealjs/notes/plugin.js'

function replaceWithSection(el) {
    let sec = document.createElement("section");
    sec.innerHTML = el.innerHTML;
    for (const name of el.getAttributeNames()) {
        const value = el.getAttribute(name);
        sec.setAttribute(name, value);
    }
    el.parentNode.replaceChild(sec, el);
}

let content = document.getElementById("content");
let parent = content.parentNode;
let wrapper = document.createElement("div");
for (const el of document.getElementsByTagName("header")) {
    parent.insertBefore(el, content);
}
parent.insertBefore(wrapper, content);
wrapper.appendChild(content);
document.querySelectorAll("div.outline-3").forEach(replaceWithSection);
document.querySelectorAll("div.outline-2").forEach(replaceWithSection);
content.classList.add("slides");
wrapper.classList.add("reveal");
wrapper.style.height = "378px";

Reveal.initialize({
    width: 1920,
    height: 1080,
    hash: true,
    controls: false,
    slideNumber: 'c',
    transition: "none",
    viewDistance: 4,
    hideCursorTime: 1000,
    embedded: true,
    //disableLayout: true,
    center: false,
    margin: 0,
    pdfMaxPagesPerSlide: 1,
    //pdfSeparateFragments: false,
    plugins: [ RevealNotes ]
});
Reveal.addEventListener("ready", function addFragmentToLists() {
  for (const listItem of document.querySelectorAll(".fragmented-list li")) {
    listItem.classList.add("fragment");
  }
});
/*
Reveal.on('ready', event => {
    Reveal.configure({ slideNumber: !(event.indexh === 0) });
});
Reveal.addEventListener('slidechanged', (event) => {
    Reveal.configure({ slideNumber: !(event.indexh === 0) });
});
*/

svg favicon

<svg width="86pt" height="86pt" version="1.1" viewBox="0 0 86 86" xmlns="http://www.w3.org/2000/svg">
<style>
@media (prefers-color-scheme: dark) {
.grid {
stroke: white;
fill: none;
stroke-width: 2.1918;
stroke-miterlimit: 10;
clip-path: url(#clip);
}
.node {fill: white;}
.outl {
stroke: white;
fill: none;
stroke-width: .797;
stroke-miterlimit: 10;
}
.arro {
stroke: white;
fill: white;
stroke-width: .797;
stroke-miterlimit: 10;
}
}
@media (prefers-color-scheme: light) {
.grid {
stroke: black;
fill: none;
stroke-width: 2.1918;
stroke-miterlimit: 10;
clip-path: url(#clip);
}
.node {fill: black;}
.outl {
stroke: black;
fill: none;
stroke-width: .797;
stroke-miterlimit: 10;
}
.arro {
stroke: black;
fill: black;
stroke-width: .797;
stroke-miterlimit: 10;
}
}
</style>
<defs>
<clipPath id="clip">
<path d="m61.578 29.039-36.824-21.258-36.824 21.258v42.52l36.824 21.262 36.824-21.262z"/>
</clipPath>
</defs>
<g id="page" transform="translate(18.246 -7.3008)">
<path class="grid" d="m81.445 50.301h-56.691" clip-path="url(#clip)"/>
<path class="grid" d="m53.098 1.2031-28.344 49.098" clip-path="url(#clip)"/>
<path class="grid" d="m-3.5937 1.2031 28.348 49.098" clip-path="url(#clip)"/>
<path class="grid" d="m-31.941 50.301h56.695" clip-path="url(#clip)"/>
<path class="grid" d="m-3.5937 99.398 28.348-49.098" clip-path="url(#clip)"/>
<path class="grid" d="m53.098 99.398-28.344-49.098" clip-path="url(#clip)"/>
<path class="grid" d="m67.273 25.75h-85.039l42.52 73.648z" clip-path="url(#clip)"/>
<path class="grid" d="m24.754 1.2031-42.52 73.645h85.039z" clip-path="url(#clip)"/>
<path class="node" d="m57.066 50.301c0-2.1914-1.7734-3.9688-3.9687-3.9688-2.1914 0-3.9649 1.7773-3.9649 3.9688s1.7735 3.9688 3.9649 3.9688c2.1953 0 3.9687-1.7773 3.9687-3.9688z"/>
<path class="node" d="m42.894 25.75c0-2.1914-1.7773-3.9688-3.9687-3.9688s-3.9688 1.7774-3.9688 3.9688 1.7774 3.9687 3.9688 3.9687 3.9687-1.7773 3.9687-3.9687z"/>
<path class="node" d="m14.547 25.75c0-2.1914-1.7774-3.9688-3.9688-3.9688s-3.9687 1.7774-3.9687 3.9688 1.7773 3.9687 3.9687 3.9687 3.9688-1.7773 3.9688-3.9687z"/>
<path class="node" d="m0.375 50.301c0-2.1914-1.7773-3.9688-3.9687-3.9688-2.1915 0-3.9688 1.7773-3.9688 3.9688s1.7773 3.9688 3.9688 3.9688c2.1914 0 3.9687-1.7773 3.9687-3.9688z"/>
<path class="node" d="m14.547 74.848c0-2.1915-1.7774-3.9688-3.9688-3.9688s-3.9687 1.7773-3.9687 3.9688c0 2.1914 1.7773 3.9687 3.9687 3.9687s3.9688-1.7773 3.9688-3.9687z"/>
<path class="node" d="m42.894 74.848c0-2.1915-1.7773-3.9688-3.9687-3.9688s-3.9688 1.7773-3.9688 3.9688c0 2.1914 1.7774 3.9687 3.9688 3.9687s3.9687-1.7773 3.9687-3.9687z"/>
<path class="node" d="m28.723 50.301c0-2.1914-1.7774-3.9688-3.9688-3.9688-2.1914 0-3.9687 1.7773-3.9687 3.9688s1.7773 3.9688 3.9687 3.9688c2.1914 0 3.9688-1.7773 3.9688-3.9688z"/>
<path class="outl" d="m61.578 29.039-36.824-21.258-36.824 21.258v42.52l36.824 21.262 36.824-21.262z"/>
<path class="arro" d="m43.812 18.781 2.8086 3.4375-0.40625-2.0469 1.9766-0.67188z"/>
<path class="arro" d="m38.117 15.496 2.8125 3.4375-0.41016-2.0508 1.9805-0.67187z"/>
<path class="arro" d="m10.086 16.25 4.3828-0.71875-1.9805-0.67188 0.41015-2.0469z"/>
<path class="arro" d="m4.3906 19.535 4.3828-0.71485-1.9766-0.67187 0.40625-2.0508z"/>
<path class="arro" d="m-1.3047 22.824 4.3828-0.71875-1.9766-0.67188 0.40625-2.0469z"/>
<path class="arro" d="m-12.07 47.855 1.5703 4.1523-1.5703-1.3789-1.5703 1.3789z"/>
<path class="arro" d="m0.24218 78.668 4.3789 0.71485-1.9766 0.67187 0.40625 2.0508z"/>
<path class="arro" d="m5.9336 81.957 4.3828 0.71485-1.9766 0.67187 0.40625 2.0469z"/>
<path class="arro" d="m35.691 86.504 2.8125-3.4375-0.40625 2.0508 1.9766 0.67187z"/>
<path class="arro" d="m41.387 83.215 2.8125-3.4336-0.41015 2.0469 1.9805 0.67188z"/>
<path class="arro" d="m47.082 79.93 2.8086-3.4375-0.40625 2.0469 1.9805 0.67188z"/>
<path class="arro" d="m61.578 48.441-1.5703 4.1524 1.5703-1.3789 1.5703 1.3789z"/>
</g>
</svg>

Preview with simple-httpd

(require 'simple-httpd)

(defun httpd-gen-path (path &optional root)
  "Translate GET to secure path in ROOT (`httpd-root')."
  (let ((clean (expand-file-name (httpd-clean-path path) (or root httpd-root))))
    (if (file-directory-p clean)
        (let* ((dir (file-name-as-directory clean))
               (indexes (cl-mapcar (apply-partially 'concat dir) httpd-indexes))
               (existing (cl-remove-if-not 'file-exists-p indexes)))
          (or (car existing) dir))
      (if (file-exists-p clean)
          clean
        (concat clean ".html")))))

(setq httpd-root my-website-publish-dir)
(setq httpd-listings t)
(setq httpd-host "0.0.0.0")
(httpd-start)

references