Publishing with org-mode
Table of Contents
Intended for use with: Emacs configuration for writing.
(setq package-user-dir (expand-file-name "~/.config/emacs-writing/elpa"))
(package-initialize)
(require 's)
(require 'org)
(require 'ox)
(require 'ox-publish)
(require 'htmlize)
(setq org-html-htmlize-output-type 'css
org-html-head-include-default-style nil)
(require 'org-mem)
(setq org-mem-watch-dirs '("~/giabao.xyz/org/"))
(setq org-mem-do-sync-with-org-id t)
(org-mem-updater-mode)
(require 'org-mem-roamy)
(org-mem-roamy-db-mode)
(require 'org-node)
(org-node-cache-mode)
(require 'org-node-backlink)
(setq org-node-backlink-drawer-formatter
#'org-node-backlink-format-as-bullet-with-time)
(defun org-node-backlink--fix-nearby-drawer (&optional remove)
"Update nearby backlinks drawer so it reflects current reality.
If REMOVE non-nil, remove it instead."
(if remove
(org-node--delete-drawer "BACKLINKS")
(let* ((id (org-entry-get nil "ID"))
(entry (org-mem-entry-by-id id))
(portal-ids (mapcar #'car
(emacsql (org-mem-roamy-db)
[:select node_id
:from tags
:where (in tag $v1)]
(vconcat org-excluded-tags))))
(all-origin-ids (when entry
(thread-last
(append (org-mem-id-links-to-entry entry)
(org-mem-roam-reflinks-to-entry entry))
(seq-keep #'org-mem-link-nearby-id)
(delete id)
(delete-dups)
;; Shouldn't be necessary if tables are correct, but
;; don't assume `org-mem-updater-mode' is flawless
(seq-filter #'org-mem-entry-by-id))))
(origin-ids (seq-difference all-origin-ids portal-ids))
(org-node-backlink--inhibit-flagging t))
(if (null origin-ids)
(org-node--delete-drawer "BACKLINKS")
(save-excursion
(save-restriction
(org-node-narrow-to-drawer-create
"BACKLINKS" org-node-backlink-drawer-positioner)
(let* ((col (current-indentation))
(lines (split-string (buffer-string) "\n" t))
(already-present-ids
(seq-keep #'org-node-backlink--extract-id lines))
(to-add (seq-difference origin-ids already-present-ids))
(to-remove (seq-difference already-present-ids origin-ids))
(to-reformat (seq-intersection already-present-ids origin-ids)))
;; Add new, remove stale, reformat the rest
(dolist (id to-remove)
(save-excursion
(search-forward id)
(delete-line)))
(dolist (id to-reformat)
(save-excursion
(search-forward id)
(back-to-indentation)
(let* ((line (buffer-substring-no-properties (point) (pos-eol)))
(reformatted (org-node-backlink--reformat-line line)))
(unless (equal line reformatted)
(atomic-change-group
(delete-region (point) (pos-eol))
(insert reformatted))))))
(dolist (id to-add)
(let ((title (org-mem-title-maybe (org-mem-entry-by-id id))))
(insert (funcall org-node-backlink-drawer-formatter id title))
(newline)
(indent-to col)))
;; Membership is correct, now re-sort
(let ((sorted-lines (sort (split-string (buffer-string) "\n" t)
org-node-backlink-drawer-sorter)))
(when org-node-backlink-drawer-sort-in-reverse
(setq sorted-lines (nreverse sorted-lines)))
(unless (equal lines sorted-lines)
(atomic-change-group
(delete-region (point-min) (point-max))
(insert (string-join sorted-lines "\n"))))))))))))
1. org-mode
(defvar org-excluded-tags)
(setq org-excluded-tags '("portal" "tag"))
(setq org-html-mathjax-template "")
(setq org-link-abbrev-alist
'(("img" . "https://giabao.xyz/img/%s")
("vid" . "https://giabao.xyz/vid/%s")))
2. org-publish
(defvar org-publish-dir)
(defvar org-publish-url)
(setq org-publish-dir "/home/bao/giabao.xyz/")
(setq org-publish-url "https://giabao.xyz")
2.1. Preprocessing
(defun my/org-preprocess-block-title (backend)
"Preprocess custom block's title."
(when (org-export-derived-backend-p backend 'html)
(goto-char (point-min))
(while (re-search-forward "^#\\+begin_\\([a-z]+\\) \\[\\([a-zA-Z0-9,='(/)\{\}\\ -]+\\)\\]" nil t)
(let ((env (match-string 1))
(name (match-string 2)))
(replace-match "")
(insert (format "#+attr_html: :title %s\n#+begin_%s" name env))))))
(defun my/org-export-format-backlinks-drawer (backend)
"Preprocess :BACKLINKS: drawer."
(when (org-export-derived-backend-p backend 'html)
(save-excursion
(goto-char (point-min))
;; Search for :BACKLINKS: drawer syntax
(while (re-search-forward "^[ \t]*:BACKLINKS:[ \t]*$" nil t)
(let ((drawer-start (match-beginning 0))
(contents-start (line-beginning-position 2))
(drawer-end (re-search-forward "^[ \t]*:END:[ \t]*$" nil t)))
(when drawer-end
(let ((contents (buffer-substring-no-properties contents-start (match-beginning 0))))
;; Replace drawer with a custom block
(delete-region drawer-start (point))
(goto-char (point-max))
(insert (format "* Backlinks
:PROPERTIES:
:UNNUMBERED: notoc
:END:
%s" contents)))))))))
(setq org-export-before-processing-functions
'(my/org-preprocess-block-title
my/org-export-format-backlinks-drawer))
2.2. Content processing
(defun my/org-html--build-head (plist)
(let* ((dir (expand-file-name (plist-get plist :base-directory)))
(org-css (file-relative-name "css/org.css" dir))
(math-css (file-relative-name "css/math.css" dir)))
(concat (format "<link rel=\"stylesheet\" href=\"%s\" type=\"text/css\" />
<link rel=\"stylesheet\" href=\"%s\" type=\"text/css\" />\n" org-css math-css)
"<link rel=\"preload\" as=\"image\" href=\"https://giabao.xyz/img/background.gif\" fetchpriority=\"high\">
<link rel=\"preload\" as=\"image\" href=\"https://giabao.xyz/img/border.png\" fetchpriority=\"high\">
<link rel=\"preload\" as=\"image\" href=\"https://giabao.xyz/img/journal/grid.png\" fetchpriority=\"high\">
<link rel=\"preload\" as=\"image\" href=\"https://giabao.xyz/img/img-border.gif\" fetchpriority=\"high\">
"
"<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">
<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>
<link href=\"https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap\" rel=\"stylesheet\">
"
;; "<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">
;; <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>
;; <link href=\"https://fonts.googleapis.com/css2?family=Merriweather:ital,opsz,wght@0,18..144,300..900;1,18..144,300..900&display=swap\" rel=\"stylesheet\">
;; "
"<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/katex@0.18.4/dist/katex.min.css\" integrity=\"sha384-u1zONI5gPXUx0UKI62c75/zww972y0v2rSK5ZYlVdS6xEuWDeZWUI66v6t1gvlXJ\" crossorigin=\"anonymous\">
<script defer src=\"https://cdn.jsdelivr.net/npm/katex@0.18.4/dist/katex.min.js\" integrity=\"sha384-ykMNcWQhhTUb0YV9SPpPUFURHZ+tWmubkakGBP+OgNK/UXdO2gtzglWx0Rj9hnO3\" crossorigin=\"anonymous\"></script>
<script defer src=\"https://cdn.jsdelivr.net/npm/katex@0.18.4/dist/contrib/auto-render.min.js\" integrity=\"sha384-bjyGPfbij8/NDKJhSGZNP/khQVgtHUE5exjm4Ydllo42FwIgYsdLO2lXGmRBf5Mz\" crossorigin=\"anonymous\"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
renderMathInElement(document.body, {
macros: {
\"\\\\arccot\": \"\\\\operatorname{arccot}\"
}
});
});
</script>
")))
(defun org-html--tags (tags info)
"Format TAGS into HTML.
INFO is a plist containing export options."
(when tags
(format "<span class=\"tag\">%s</span>"
(mapconcat
(lambda (tag)
(format "<span class=\"tag-%s\">%s</span>"
(concat (plist-get info :html-tag-class-prefix)
(org-html-fix-class-name tag))
tag))
tags " "))))
2.3. Postprocessing
(defun my/org-export-filetags-inject (text backend info)
"Extract #+FILETAGS and inject an HTML string into the final output."
(if (org-export-derived-backend-p backend 'html)
(let* ((all-tags (org-node--get-filetags))
(tags (seq-difference all-tags org-excluded-tags)))
;; Use org-node--get-filetags
(if tags
(let ((tags-html (concat "<div id=\"filetags\">\n"
"<ul id=\"filetags-list\"><li id=\"filetags-text\">Tags:</li>"
(mapconcat (lambda (tag)
(format "<li><a href=\"%s/tags/tag-%s.html\" class=\"tag\">#%s</a></li>"
org-publish-url tag tag))
tags "\n")
"</ul>"
"</div>\n")))
(replace-regexp-in-string "</h1>" (concat "</h1>\n" tags-html) text))
text))
text))
(setq org-export-filter-final-output-functions
'(my/org-export-filetags-inject))
2.4. Template
(defun org-html--build-navbar (info)
"Return navbar HTML for exported document.
INFO is a plist used as a communication channel."
(let* ((file (file-name-base
(plist-get info :input-file)))
(dir (file-name-nondirectory
(directory-file-name
(file-name-directory (plist-get info :input-file)))))
(buttons '("home" "journal" "notes"))
(buttons-html
(mapconcat
(lambda (button)
(if (or (string-equal-ignore-case button file) (string-equal-ignore-case button dir))
(format " <li class=\"navbar-active\">\n <a class=\"navbar-link\" href=\"%s/%s.html\">%s</a>\n </li>"
org-publish-url button (s-capitalize button))
(format " <li class=\"navbar-item\">\n <a class=\"navbar-link\" href=\"%s/%s.html\">%s</a>\n </li>"
org-publish-url button (s-capitalize button))))
buttons "\n")))
(concat
(format "<div id=\"navbar\">
<ul class=\"navbar-left\">
%s
</ul>" buttons-html)
(format "<ul class=\"navbar-right\">
<li class=\"navbar-item\">
<a class=\"navbar-link\" href=\"%s/tags.html\">Tags</a>
</li>
</ul>
</div>" org-publish-url))))
(defun my/org-html-template (contents info)
"Return complete document string after HTML conversion.
CONTENTS is the transcoded contents string. INFO is a plist
holding export options."
(concat
(when (and (not (org-html-html5-p info)) (org-html-xhtml-p info))
(let* ((xml-declaration (plist-get info :html-xml-declaration))
(decl (or (and (stringp xml-declaration) xml-declaration)
(cdr (assoc (plist-get info :html-extension)
xml-declaration))
(cdr (assoc "html" xml-declaration))
"")))
(when (not (or (not decl) (string= "" decl)))
(format "%s\n"
(format decl
(or (and org-html-coding-system
(coding-system-get org-html-coding-system :mime-charset))
"iso-8859-1"))))))
(org-html-doctype info)
"\n"
(concat "<html"
(cond ((org-html-xhtml-p info)
(format
" xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"%s\" xml:lang=\"%s\""
(plist-get info :language) (plist-get info :language)))
((org-html-html5-p info)
(format " lang=\"%s\"" (plist-get info :language))))
">\n")
"<head>\n"
(org-html--build-meta-info info)
(org-html--build-head info)
(org-html--build-mathjax-config info)
"</head>\n"
"<body>\n"
(org-html--build-navbar info)
(let ((link-up (org-trim (plist-get info :html-link-up)))
(link-home (org-trim (plist-get info :html-link-home))))
(unless (and (string= link-up "") (string= link-home ""))
(format (plist-get info :html-home/up-format)
(or link-up link-home)
(or link-home link-up))))
;; Preamble.
(org-html--build-pre/postamble 'preamble info)
;; Document contents.
"<div id=\"content-container\">"
(let ((div (assq 'content (plist-get info :html-divs))))
(format "<%s id=\"%s\" class=\"%s\">\n"
(nth 1 div)
(nth 2 div)
(plist-get info :html-content-class)))
;; Document title.
(when (plist-get info :with-title)
(let ((title (and (plist-get info :with-title)
(plist-get info :title)))
(subtitle (plist-get info :subtitle))
(html5-fancy (org-html--html5-fancy-p info)))
(when title
(format
(if html5-fancy
"<header>\n<h1 class=\"title\">%s</h1>\n%s</header>"
"<h1 class=\"title\">%s%s</h1>\n")
(org-export-data title info)
(if subtitle
(format
(if html5-fancy
"<p class=\"subtitle\" role=\"doc-subtitle\">%s</p>\n"
(concat "\n" (org-html-close-tag "br" nil info) "\n"
"<span class=\"subtitle\">%s</span>\n"))
(org-export-data subtitle info))
"")))))
contents
(format "</%s>\n" (nth 1 (assq 'content (plist-get info :html-divs))))
"</div>"
;; Postamble.
"<div id=\"postamble-container\">"
(org-html--build-pre/postamble 'postamble info)
"</div>"
;; Possibly use the Klipse library live code blocks.
(when (plist-get info :html-klipsify-src)
(concat "<script>" (plist-get info :html-klipse-selection-script)
"</script><script src=\""
org-html-klipse-js
"\"></script><link rel=\"stylesheet\" type=\"text/css\" href=\""
org-html-klipse-css "\"/>"))
;; Closing document.
"</body>\n</html>"))
(org-export-define-derived-backend 'my/html 'html
:translate-alist '((template . my/org-html-template)))
(defun my/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."
(org-publish-org-to 'my/html filename
(concat (when (> (length org-html-extension) 0) ".")
(or (plist-get plist :html-extension)
org-html-extension
"html"))
plist pub-dir))
2.5. Journal-specific export
(defun my/org-html-journal-headline-transcoder (headline contents info)
"Transcode level 1 HEADLINE into a details/summary block."
(let ((level (org-element-property :level headline)))
;; Check if it is a top-level (first level) heading
(if (= level 1)
(let ((title (org-export-data (org-element-property :title headline) info)))
(format "\n<details class=\"org-details\">\n<summary class=\"org-summary\">%s</summary>\n%s\n</details>\n"
title (or contents "")))
;; Fall back to standard HTML export behavior for levels 2+
(org-export-with-backend 'html headline contents info))))
(org-export-define-derived-backend 'my/html-journal 'html
:translate-alist '((headline . my/html-journal-headline-transcoder)
(template . my/org-html-template)))
(defun my/org-html-publish-journal-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."
(org-publish-org-to 'my/html-journal filename
(concat (when (> (length org-html-extension) 0) ".")
(or (plist-get plist :html-extension)
org-html-extension
"html"))
plist pub-dir))
2.6. org-node backlink sync
(ignore-error user-error
(org-node-backlink-mass-update-drawers))
(org-save-all-org-buffers)
2.7. Project publish
(setq org-publish-project-alist
`(("css"
:base-directory "css/"
:base-extension "css"
:publishing-directory "html/css/"
:recursive t
:publishing-function org-publish-attachment)
("home and notes"
:base-directory "org/"
:base-extension "org"
:publishing-directory "html/"
:exclude ,(regexp-opt '("journal.org"))
:recursive nil
:publishing-function my/org-html-publish-to-html
:html-head my/org-html--build-head
:auto-sitemap nil)
("journal"
:base-directory "org/"
:base-extension "org"
:publishing-directory "html/"
:exclude ,(regexp-opt '("home.org" "notes.org"))
:recursive nil
:publishing-function my/org-html-publish-journal-to-html
:html-head my/org-html--build-head
:auto-sitemap nil)
("notes"
:base-directory "org/notes/"
:base-extension "org"
:publishing-directory "html/notes/"
:recursive nil
:publishing-function my/org-html-publish-to-html
:html-head my/org-html--build-head
:auto-sitemap nil)
("all" :components ("home and notes"
"css"
"journal"
"notes"))))
3. Uploading
rsync -rtv --no-g --delete /home/bao/giabao.xyz/html/* giabao.xyz:/var/www/html/giabao.xyz/
rsync -rtv --no-g --delete /home/bao/giabao.xyz/img/ giabao.xyz:/var/www/html/giabao.xyz/