Custom org-sitemap-function
post-Org 9.1
Published: December 27, 2022
I have been lugging around an old version of org-mode (9.0 to be specific) in the git repo which builds this website for a number of years now. I decided to do this because I had a custom org-sitemap-function
to generate the landing page for my blog, but org 9.1 introduced a breaking change to the org-publish API.
I have now finally come around to fixing this issue and making my website compatible with modern emacs and org-mode versions higher than 9.1. However, porting my old sitemap function was… surprisingly difficult? So just in case someone is looking up my original post these days, this post contains a sitemap-function
which will work in 2022.
In the pre-9.1 version, the org-sitemap-function
would get the project-plist
as argument. Post-9.1, the org manual states:
:sitemap-function
Plug-in function to use for generation of the sitemap. It is called with two arguments: the title of the site-map and a representation of the files and directories involved in the project as a nested list, which can further be transformed using org-list-to-generic, org-list-to-subtree and alike. Default value generates a plain list of links to all files in the project.
This makes it a little more difficult to produce the sitemap page that we want, but I was able to get it done by parsing each element of the list and extracting the path to the filename with the following function:
(defun my-blog-parse-sitemap-list (l) "Convert the sitemap list in to a list of filenames." (mapcar #'(lambda (i) (let ((link (with-temp-buffer (let ((org-inhibit-startup nil)) (insert (car i)) (org-mode) (goto-char (point-min)) (org-element-link-parser))))) (when link (plist-get (cadr link) :path)))) (cdr l)))
Finally, the new and improved sitemap function looks like this:
(defun my-blog-sort-article-list (l p) "sort the article list anti-chronologically." (sort l #'(lambda (a b) (let ((d-a (org-publish-find-date a p)) (d-b (org-publish-find-date b p))) (not (time-less-p d-a d-b)))))) (defun my-blog-sitemap (title list) "Generate the landing page for my blog." (with-temp-buffer ;; mangle the parsed list given to us into a plain lisp list of files (let* ((filenames (my-blog-parse-sitemap-list list)) (project-plist (assoc "blog-articles" org-publish-project-alist)) (articles (my-blog-sort-article-list filenames project-plist))) (dolist (file filenames) (let* ((abspath (file-name-concat my-website-blog-dir file)) (relpath (file-relative-name abspath my-website-base-dir)) (title (org-publish-find-title file project-plist)) (date (format-time-string (car org-time-stamp-formats) (org-publish-find-date file project-plist))) (preview (my-blog-get-preview abspath))) ;; insert a horizontal line before every post, kill the first one ;; before saving (insert "-----\n") (insert (concat "* [[file:" relpath "][" title "]]\n")) ;; add properties for `ox-rss.el' here (let ((rss-permalink (concat (file-name-sans-extension relpath) ".html")) (rss-pubdate date)) (org-set-property "RSS_PERMALINK" rss-permalink) (org-set-property "PUBDATE" rss-pubdate)) ;; insert the date, preview, & read more link (insert (concat "Published: " date "\n\n")) (insert preview) (insert "\n") (insert (concat "[[file:" relpath "][Read More...]]\n")))) ;; kill the first hrule to make this look OK (goto-char (point-min)) (let ((kill-whole-line t)) (kill-line)) ;; insert a title and save (insert "#+OPTIONS: title:nil\n") (insert "#+TITLE: Blog - Dennis Ogbe's Personal Website\n") (insert "#+AUTHOR: Dennis Ogbe\n") (insert "#+EMAIL: [email protected]\n") (buffer-string))))
This info can be combined with the instructions in my original post to cook up your own very special org-mode website.
Update emacs --batch
, i.e., something like:
emacs --batch -l "./project.el" --eval="(org-publish \"blog\" t)"
As part of the build process, I use CSSTidy to minify my CSS and bibtex2html to generate the list of publications.
;; This file defines the org-publish project for my web site. -*- eval: (flycheck-mode -1) -*- ;; I can either run this from the build.sh script / Makefile or ;; evaluate this buffer and publish from within Emacs while editing ;; the page. (defun generate-website (arg) "Generate my website. Call with prefix argument for a complete rebuild." (interactive "P") (message "Generating website for staging...") (if arg ;; force rebuild everything (org-publish "blog" t nil) ;; only rebuild what changed (org-publish "blog" nil nil)) (message "Done. Check output in %s" my-website-out-dir)) ;; I am not sure why I have to do it this way... This snippet finds ;; the parent directory of this file, which is the base directory of ;; the project. (setq my-website-base-dir (file-name-as-directory (file-name-directory (directory-file-name (file-name-directory (or load-file-name buffer-file-name)))))) ;; set up the rest of the directory tree (defmacro my-website-set-path-var (name) (list 'setq (intern (format "my-website-%s-dir" name)) (list 'file-name-as-directory (concat my-website-base-dir name)))) (my-website-set-path-var "bin") (my-website-set-path-var "bib") (my-website-set-path-var "blog") (my-website-set-path-var "css") (my-website-set-path-var "cv") (my-website-set-path-var "dl") (my-website-set-path-var "html") (my-website-set-path-var "img") (my-website-set-path-var "lisp") (my-website-set-path-var "pages") ;; we pull the output directory out of an environment variable. If this ;; variable is not set, we bail (setq my-website-out-dir (getenv "WEBSITE_OUT_DIR")) (unless my-website-out-dir (setq my-website-out-dir (file-name-concat my-website-base-dir "www")) (message "Using default WEBSITE_OUT DIR: %s" my-website-out-dir)) (setq my-website-out-dir (file-name-as-directory my-website-out-dir)) ; https://adam.kruszewski.name/2022-05-08-org-publish-call-org-fold-core-region-error.html ; (setq org-fold-core-style 'text-properties) ;; [2022-12-27 Tue] This is now compatible with emacs 28.1. it ;; requires the `htmlize' and `org-contrib' packages. (package-initialize) (require 'org) (require 'htmlize) (require 'org-contrib) (require 'ox-html) (require 'ox-rss) (require 'oc-csl) (require 'citeproc) (require 'parsebib) (require 'rx) ;; fix obsolete `font-lock-reference-face' ;; (see https://lists.nongnu.org/archive/html/emacs-devel/2022-09/msg01022.html) (setq font-lock-reference-face font-lock-constant-face) ;; re-build the entire project if $WEBSITE_BUILD_TYPE=FULL (when (and (getenv "WEBSITE_BUILD_TYPE") (string-equal (downcase (getenv "WEBSITE_BUILD_TYPE")) "full")) (setq org-publish-use-timestamps-flag nil)) ;; references -------------------------------------------------------- (setq org-cite-export-processors `((t . (csl ,(file-name-concat my-website-bib-dir "ieee-mod.csl"))))) ;; html export settings ---------------------------------------------- (setq org-export-html-coding-system 'utf-8-unix) (setq org-html-htmlize-output-type 'css) (setq org-html-doctype "xhtml5") (setq org-html-html5-fancy t) (setq org-export-time-stamp-file nil) (setq org-rss-use-entry-url-as-guid nil) ;; massage org-time-stamps (setq org-time-stamp-custom-formats '("%B %d, %Y" . "%B %d, %Y, %H:%M")) (setq org-display-custom-times t) (setq org-export-date-timestamp-format (car org-time-stamp-custom-formats)) (defun my-org-export-ensure-custom-times (backend) (setq-local org-display-custom-times t) (setq-local org-export-date-timestamp-format (car org-time-stamp-custom-formats))) (add-hook 'org-export-before-processing-hook 'my-org-export-ensure-custom-times) ;; <2024-03-10 Sun> Need to fix `org-html-timestamp', since it calls ;; `org-timestamp-translate', which does not strip brackets.. why. (defun my-format-timestamp (ts) "Format a time stamp using org-mode according to `org-time-stamp-custom-formats'." (org-format-timestamp ts (org-time-stamp-format (org-timestamp-has-time-p ts) 'no-brackets t) t)) (defun override-org-html-timestamp (timestamp _contents info) (let ((value (org-html-plain-text (my-format-timestamp timestamp) info))) (format "<span class=\"timestamp-wrapper\"><span class=\"timestamp\">%s</span></span>" (replace-regexp-in-string "--" "–" value)))) (advice-add 'org-html-timestamp :override #'override-org-html-timestamp) ;; we do not need backup files for this (setq make-backup-files nil) ;; we evaluate some elisp to generate some html. this lets us do that. (setq org-confirm-babel-evaluate nil) (setq website-head (concat "<script src='/../res/code.js'></script>\n" ; some very simple javascript "<link rel='stylesheet' href='/../res/style.css' />\n" ; minified style sheet "<link rel='shortcut icon' type=\"image/ico\" href='/../img/favicon.ico'/>\n" ; favicon "<link rel='alternate' type='application/rss+xml' title='RSS Feed for ogbe.net' href='/blog-feed.xml' />\n")) ;; header and footer (defun website-header (info) (with-temp-buffer (insert-file-contents (concat my-website-html-dir "header.html")) (buffer-string))) (defun website-footer (info) (with-temp-buffer (insert-file-contents (concat my-website-html-dir "footer.html")) (buffer-string))) (defun my-blog-org-export-format-drawer (name content) (concat "<div class=\"drawer " (downcase name) "\">\n" "<h6>" (capitalize name) "</h6>\n" content "\n</div>")) (defun minify (what file) (let ((tool (file-name-concat my-website-bin-dir (cond ((eq what 'js) "rjsmin.py") ((eq what 'css) "rcssmin.py") (t (error "unknown format: %s" what)))))) (shell-command-to-string (format "python3 %s < %s" tool file)))) (defun minify-css (file) (minify 'css file)) (defun minify-js (file) (minify 'js file)) (setq my-blog-local-mathjax '((path "/mathjax/tex-chtml.js") (scale "100") (align "center") (indent "2em") (tagside "right") (autonumber "all") (mathml nil))) (setq my-blog-local-mathjax '((path "/mathjax/tex-chtml.js"))) (setq my-blog-extra-mathjax-config (concat "<script>" "MathJax = { tex: { inlineMath: [['$', '$'], ['\\\\(', '\\\\)']], tags: 'ams' }, svg: { fontCache: 'global' }};" "</script>")) (defun my-blog-get-preview (file) "The comments in FILE have to be on their own lines, prefereably before and after paragraphs." (with-temp-buffer (insert-file-contents file) (goto-char (point-min)) (let ((beg (+ 1 (re-search-forward "^#\\+BEGIN_PREVIEW$"))) (end (progn (re-search-forward "^#\\+END_PREVIEW$") (match-beginning 0)))) (buffer-substring beg end)))) (defun my-blog-parse-sitemap-list (l) "Convert the sitemap list in to a list of filenames." (mapcar #'(lambda (i) (let ((link (with-temp-buffer (let ((org-inhibit-startup nil)) (insert (car i)) (org-mode) (goto-char (point-min)) (org-element-link-parser))))) (when link (plist-get (cadr link) :path)))) (cdr l))) (defun my-blog-sort-article-list (l p) "sort the article list anti-chronologically." (sort l #'(lambda (a b) (let ((d-a (org-publish-find-date a p)) (d-b (org-publish-find-date b p))) (not (time-less-p d-a d-b)))))) (defun my-blog-sitemap (title list) "Generate the landing page for my blog. This actually generate two pages. one for the RSS output and one for the actual landing page." (let* ((filenames (my-blog-parse-sitemap-list list)) (project-plist (assoc "blog-articles" org-publish-project-alist)) (rss-output-file (file-name-concat my-website-blog-dir "blog-feed.org"))) ;; first generate the org file for the RSS output (with-temp-buffer (insert "#+AUTHOR: Dennis Ogbe\n") (insert "#+EMAIL: [email protected]\n") (insert (mapconcat (lambda (file) (let* ((abspath (file-name-concat my-website-blog-dir file)) (relpath (file-relative-name abspath my-website-base-dir)) (title (org-publish-find-title file project-plist)) (date (format-time-string (car org-time-stamp-formats) (org-publish-find-date file project-plist))) (rss-permalink (file-name-sans-extension relpath)) (preview (my-blog-get-preview abspath))) (with-temp-buffer ;; need to make this an org-mode buffer (org-mode) ;; insert the link to the article as h2 (insert (concat "* [[file:" relpath "][" title "]]\n")) ;; add properties for `ox-rss.el' here (org-set-property "RSS_PERMALINK" rss-permalink) (org-set-property "PUBDATE" date) (org-set-property "RSS_TITLE" title) ;; insert the preview (insert preview) (buffer-string)))) filenames "\n")) (write-file rss-output-file)) ;; now generate the actual Blog landing page (with-temp-buffer ;; insert a title and save (insert "#+TITLE: Blog - Dennis Ogbe's Personal Website\n") (insert "#+AUTHOR: Dennis Ogbe\n") (insert "#+EMAIL: [email protected]\n\n") (insert "#+OPTIONS: title:nil\n") (insert "@@html:<h1>Blog</h1>@@\n\n") ; this way the browser's tab shows ^ but the site shows < ;; only display a full preview for the first 10 posts (let* ((nfull 10) (title-preview (seq-subseq filenames 0 (1- nfull))) (title-only (seq-subseq filenames nfull))) (insert (mapconcat (lambda (file) (let* ((abspath (file-name-concat my-website-blog-dir file)) (relpath (file-relative-name abspath my-website-base-dir)) (title (org-publish-find-title file project-plist)) (date (format-time-string (car org-time-stamp-custom-formats) (org-publish-find-date file project-plist))) (preview (my-blog-get-preview abspath))) (with-temp-buffer ;; insert the link to the article as h2 (insert (concat "* [[file:" relpath "][" title "]]\n")) ;; insert the date, preview, and read more link (insert (concat "/Published: " date "/\n\n")) (insert preview) (insert "\n") (insert (concat "[[file:" relpath "][/Read More.../]]\n")) (buffer-string)))) title-preview "\n-----\n")) ;; For the remaining articles, show them as a list (insert "\n-----\n") (insert "@@html:<h1>Archive</h1>@@\n\n") (insert (mapconcat (lambda (file) (let* ((abspath (file-name-concat my-website-blog-dir file)) (relpath (file-relative-name abspath my-website-base-dir)) (title (org-publish-find-title file project-plist)) (date (format-time-string "%Y-%m-%d" (org-publish-find-date file project-plist)))) (format "- %s: [[file:%s][%s]]" date relpath title))) title-only "\n"))) (buffer-string)))) ;; pre- and post-processing (defun my-blog-pages-preprocessor (project-plist) (message "In the pages preprocessor.")) (defun my-blog-pages-postprocessor (project-plist) (message "In the pages postprocessor.")) (defun my-blog-articles-preprocessor (project-plist) (message "In the articles preprocessor.")) (defun my-blog-articles-postprocessor (project-plist) "Massage the sitemap file and move it up one directory. for this to work, we have already fixed the creation of the relative link in the sitemap-publish function" (let* ((sitemap-fn (concat (file-name-sans-extension (plist-get project-plist :sitemap-filename)) ".html")) (sitemap-olddir (plist-get project-plist :publishing-directory)) (sitemap-newdir (expand-file-name (concat (file-name-as-directory sitemap-olddir) ".."))) (sitemap-oldfile (expand-file-name sitemap-fn sitemap-olddir)) (sitemap-newfile (expand-file-name (concat (file-name-as-directory sitemap-newdir) sitemap-fn)))) (with-temp-buffer (goto-char (point-min)) (insert-file-contents sitemap-oldfile) ;; massage the sitemap if wanted ;; delete the old file and write the correct one (delete-file sitemap-oldfile) (write-file sitemap-newfile)))) (defun my-blog-articles-add-subheader (plist filename pub-dir) "Called after the publishing function, this adds a subheader to each blog post." (let* ((outfile (file-name-concat pub-dir (concat (file-name-base filename) ".html"))) (date (format-time-string (car org-time-stamp-custom-formats) (org-publish-find-date filename plist))) (author (org-publish-find-property filename 'author plist)) ; unused (re (regexp-quote "<h1 class=\"title\">"))) ;; open the outfile and splice publishing date into the generated HTML (with-temp-buffer (insert-file-contents outfile) (when (re-search-forward re nil t) (end-of-line) (insert (format "\n<div class=\"subheader\"><p><i>Published: %s</i></p></div>" date))) (write-file outfile)))) (defvar my-website-cssjs-files nil "A list of alists defining which files from the `css' directory to concatenate or minify.") (setq my-website-cssjs-files '(;; the main CSS style for pages ((output . "style.css") (contents . ("fonts.css.in" "code.css.in" "main.css.in")) (processor . minify-css)) ;; the CSS style for pages with margin ((output . "margin-style.css") (contents . ("margin.css.in")) (processor . minify-css)) ;; the CSS style for the RSS feed ((output . "rss.css") (contents . ("fonts.css.in" "code.css.in" "rss.css.in")) (processor . minify-css)) ;; the JavaScript code ((output . "code.js") (contents . ("code.js.in")) (processor . minify-js)))) (defun my-website-process-cssjs (project-plist) "Process the variable `my-website-cssjs-files' and produce outputs in `my-website-css-dir'." (let ((base-dir (expand-file-name (plist-get project-plist :base-directory)))) (mapcar (lambda (spec) (let ((output (file-name-concat base-dir (alist-get 'output spec)))) (with-temp-buffer (insert (mapconcat (symbol-function (alist-get 'processor spec)) (mapcar (lambda (f) (file-name-concat base-dir f)) (alist-get 'contents spec)) "\n")) (write-file output)))) my-website-cssjs-files))) (defun my-website-clean-cssjs (project-plist) "Clean up the generated outputs in `my-website-css-dir'." (let ((base-dir (expand-file-name (plist-get project-plist :base-directory)))) (mapcar (lambda (spec) (let ((output (file-name-concat base-dir (alist-get 'output spec)))) (delete-file output))) my-website-cssjs-files))) (defun my-website-rss-postprocessor (info) "Add a stylesheet tag to the RSS feed exported by `ox-rss.' This is included in more recent versions of `ox-rss', but seemingly did not make it into Emacs 28.1." (let ((outfile (file-name-concat (plist-get info :publishing-directory) "blog-feed.xml")) (stylesheet (plist-get info :my-rss-stylesheet))) (when stylesheet (with-temp-buffer (insert-file-contents outfile) (goto-char (point-min)) (replace-regexp (rx "<?xml version=\"1.0\" encoding=\"" (* anything) "\"?>") (format "\\&\n<?xml-stylesheet type=\"text/xsl\" href=\"%s\"?>" stylesheet)) (write-file outfile))))) ;; generate list of publications using `citeproc.el' (defun generate-bib-html (relfile) (let* ((infile (file-name-concat my-website-bib-dir relfile)) (csl-style (file-name-concat my-website-bib-dir "ieee-mod.csl")) (csl-locale (file-name-concat my-website-bib-dir "locales-en-US.xml")) (bib-entries ;; get all keys in the file as a list (let (keys) (maphash (lambda (k v) (push k keys)) (with-temp-buffer (insert-file-contents infile) (parsebib-collect-bib-entries :fields '("key")))) keys)) (cproc (citeproc-create csl-style (citeproc-hash-itemgetter-from-any infile) (citeproc-locale-getter-from-dir my-website-bib-dir) "en-US" t))) ;; add all keys to the citation processor (citeproc-append-citations (mapcar (lambda (k) (citeproc-citation-create :cites `(((id . ,k)))) ) bib-entries) cproc) ;; render the bibliography (with-temp-buffer (insert (car (citeproc-render-bib cproc 'html nil nil nil))) ;; highlight my name ;; FIXME should be possible with ;; `citeproc-name-postprocess-functions', but this seems to not ;; be supported in my version. (see ;; https://andras-simonyi.github.io/org-csl-cv-bib-tutorial/) (goto-char (point-min)) (replace-regexp (rx "D." (or (* " ") (* whitespace)) "Ogbe") "<b>D. Ogbe</b>") (buffer-substring (point-min) (point-max))))) ;; CUSTOM HTML BACKEND ----------------------------------------------- ;; define a custom export backend to use when publishing to HTML that ;; properly handles footnotes and margin notes. (org-export-define-derived-backend 'ogbe-html 'html :translate-alist '((footnote-reference . ogbe-html-footnote-reference) (link . ogbe-html-maybe-margin-note-link) (special-block . ogbe-html-special-block))) (defun org-html-publish-to-ogbe-html (plist filename pub-dir) "Publish an org file to using my custom backend. PLIST is the property list for the given project. FILENAME is the filename of the Org file to be published. PUB-DIR is the publishing directory. Return output file name." (org-publish-org-to 'ogbe-html filename (concat "." (or (plist-get plist :html-extension) org-html-extension "html")) plist pub-dir)) (defun ogbe-html-footnote-reference (footnote-reference contents info) (let ((text (replace-regexp-in-string ;; footnotes must have spurious <p> tags removed or they will not work "</?p.*>" "" (org-trim (org-export-data (org-export-get-footnote-definition footnote-reference info) info)))) (num (org-export-get-footnote-number footnote-reference info))) (format (concat "<span id=\"fnr.%d\" class=\"sidenote-number\">" "<a href=\"#fn.%d\"><sup>%s</sup></a></span>" "<span class=\"sidenote\" id=\"sn.%d\">" "<span><sup>%d</sup></span>" "%s</span>") num num num num num text))) (defun ogbe-html-format-margin-note (text id) (format "<span class=\"marginnote\" id=\"mn.%s\">%s</span>" id text)) (defun ogbe-html-maybe-margin-note-link (link desc info) ;; check if a link points to a #+BEGIN_margin ... #+END_margin ;; block. if yes, insert it as margin note inline. (let* ((path (org-element-property :path link)) (type (org-element-property :type link)) ;; check whether we are at a margin link. (margin-link (and (string= "fuzzy" type) (string= "mn" (car (split-string path ":"))))) ;; attempt to follow the link and check if we are looking at a margin block (margin-block (when (and (string= "fuzzy" type) (not margin-link)) (let ((elem (org-export-resolve-fuzzy-link link info))) (when (and (eq (org-element-type elem) 'special-block) (string= "margin" (org-element-property :type elem))) elem))))) (cond (margin-link ;; for a link-style margin note, replicate the behavior of ;; `ox-tufte.el' and just copy the link text without any ;; processing. (let ((text (replace-regexp-in-string "</?p.*>" "" desc)) (id (cadr (split-string path ":")))) (ogbe-html-format-margin-note text id))) (margin-block ;; for a rich block-style margin note, we need to export ;; the contents of the block and insert it inline. since at ;; this point, we do NOT want to skip the translation of ;; the "margin" blocks, we need to export the block with an ;; anonymous backend (see ;; https://orgmode.org/worg/dev/org-export-reference.html) (let ((text (org-export-data-with-backend margin-block (org-export-create-backend :parent 'ogbe-html :transcoders `((special-block . ,(lambda (block contents info) contents)) (paragraph . ogbe-html-margin-paragraph) (src-block . ogbe-html-margin-src-block))) info)) (id (org-element-property :name margin-block))) (ogbe-html-format-margin-note text id))) (t (org-html-link link desc info))))) (defun ogbe-html-special-block (special-block contents info) "Transcode a SPECIAL-BLOCK element from Org to HTML. CONTENTS holds the contents of the block. INFO is a plist holding contextual information. This function implements some special cases. For example, for `margin' blocks, we do not output anything, since they are handled by `ogbe-html-maybe-margin-note-link.'" (let ((block-type (org-element-property :type special-block))) (cond ((string= block-type "margin") "") (t (org-html-special-block special-block contents info))))) (defun ogbe-html-margin-paragraph (paragraph contents info) "Transcode a PARAGRAPH element from Org to HTML. CONTENTS is the contents of the paragraph, as a string. INFO is the plist used as a communication channel. This is a modified version of `org-html-paragraph' which does not wrap the figure in a <figure> tag, but instead uses a custom span element." (let* ((parent (org-export-get-parent paragraph)) (parent-type (org-element-type parent)) (style '((footnote-definition " class=\"footpara\"") (org-data " class=\"footpara\""))) (attributes (org-html--make-attribute-string (org-export-read-attribute :attr_html paragraph))) (extra (or (cadr (assq parent-type style)) ""))) (cond ((and (eq parent-type 'item) (not (org-export-get-previous-element paragraph info)) (let ((followers (org-export-get-next-element paragraph info 2))) (and (not (cdr followers)) (memq (org-element-type (car followers)) '(nil plain-list))))) ;; First paragraph in an item has no tag if it is alone or ;; followed, at most, by a sub-list. contents) ((org-html-standalone-image-p paragraph info) ;; insert an image with the img tag (format "<span class=\"margin-img\">%s</span>" contents)) ;; Regular paragraph. Don't insert <p> tag, since we are making a margin note (t contents)))) (defun ogbe-html-margin-src-block (src-block contents info) "Transcode a SRC-BLOCK element from Org to HTML. CONTENTS holds the contents of the item. INFO is a plist holding contextual information. This outputs a <code> tag in a <span> instead of a <pre> tag in a <div> to make this work in the margin." (let ((lang (org-element-property :language src-block)) (code (org-html-format-code src-block info)) (label (let ((lbl (org-html--reference src-block info t))) (if lbl (format " id=\"%s\"" lbl) "")))) (format "<code class=\"src src-%s margin-src-block\"%s>\n\n%s\n</code>" lang label code))) ;; override org-html-footnotes-section to generate a nicer footnotes ;; section (defun ogbe-html-footnote-section (info) "Format the footnote section. INFO is a plist used as a communication channel. This is a respin of `org-html-footnote-section.'" (let ((defs (org-export-collect-footnote-definitions info)) ;; make a table within a details block (fmt (concat "<details id=\"footnotes-details\" closed>" "<summary class=\"footnote-summary\">Footnotes</summary>\n" "<div class=\"footnote-table\">\n" "<table style=\"margin: 0 0 0 0; max-width:100%%\">\n"; table header "%s\n" ; 1 row/footnote (see later) "</table></div></details>\n"))) (when defs (format fmt (mapconcat ;; most of this stolen from `org-html-footnote-section.' (lambda (definition) (pcase definition (`(,n ,_ ,def) (let* ((inline? (not (org-element-map def org-element-all-elements #'identity nil t))) (anchor (org-html--anchor (format "fn.%d" n) n (format " class=\"footnum\" href=\"#fnr.%d\" role=\"doc-backlink\"" n) info)) (contents (org-trim (org-export-data def info))) (text (if (not inline?) contents (format "<p class=\"footpara\">%s</p>" contents)))) ;; create a HTML table row instead. (format (concat "<tr valign=\"top\">\n" ; row "<td align=\"right\" class=\"fn-tbl-number\" style=\"padding: 0.8rem 0.5rem 0 0.5rem;\">%s</td>\n" ; number "<td align=\"left\" class=\"fn-tbl-item\" style=\"padding: 0.8rem 0 0 0;\">%s</td>" ; text "</tr>") anchor text))))) defs "\n"))))) (advice-add 'org-html-footnote-section :override #'ogbe-html-footnote-section) ;; wrap list items into paragraphs to make footnotes work from lists. (defun ogbe-html-fix-list-item (item) "Wrap the contents of the list item ITEM into <p> tags." (setq item (replace-regexp-in-string "<li>" "<li><p>" item)) (setq item (replace-regexp-in-string "<dd>" "<dd><p>" item)) (setq item (replace-regexp-in-string "</li>" "</p></li>" item)) (setq item (replace-regexp-in-string "</dd>" "</p></dd>" item))) (advice-add 'org-html-format-list-item :filter-return #'ogbe-html-fix-list-item) ;; PROJECT ALIST ----------------------------------------------------- ;; define a test mode in which we only build the test page (defvar my-website-test-mode nil "Enable test mode") (when (getenv "WEBSITE_TEST_MODE") (setq my-website-test-mode t)) ;; finally, pull the project together in the `org-publish-project-alist' (setq org-publish-project-alist `(("blog" :components ,(if my-website-test-mode '("blog-css" "blog-rss" "blog-pages") '("blog-articles" "blog-rss" "blog-pages" "blog-css" "blog-images" "blog-dl"))) ("blog-articles" :base-directory ,my-website-blog-dir :base-extension "org" :publishing-directory ,(concat my-website-out-dir "blog") :publishing-function (org-html-publish-to-ogbe-html my-blog-articles-add-subheader) :preparation-function my-blog-articles-preprocessor :completion-function my-blog-articles-postprocessor :htmlized-source t ;; this enables htmlize, which means that I can use css for code! :headline-level 4 :section-numbers nil :with-toc nil :with-drawers t :with-sub-superscript nil ;; important!! ;; the following removes extra headers from HTML output -- important! :html-link-home "/" :html-head ,website-head :html-head-include-default-style nil :html-head-include-scripts nil :html-format-drawer-function my-blog-org-export-format-drawer :html-home/up-format "" :html-mathjax-options ,my-blog-local-mathjax :html-mathjax-template ,(concat my-blog-extra-mathjax-config "\n<script type=\"text/javascript\" src=\"%PATH\"></script>") :html-link-up "" :html-link-home "" :html-preamble website-header :html-postamble website-footer ;; sitemap - list of blog articles :auto-sitemap t :sitemap-filename "blog.org" :sitemap-title "Blog" :exclude "blog-feed.org" ;; custom sitemap generator function :sitemap-function my-blog-sitemap :sitemap-function org-publish-sitemap-default :sitemap-sort-files anti-chronologically :sitemap-date-format "Published: %a %b %d %Y") ("blog-pages" :base-directory ,my-website-pages-dir :base-extension ,(if my-website-test-mode "nothing" "org") :include ("test-page.org" "publications.org") :publishing-directory ,my-website-out-dir :publishing-function org-html-publish-to-ogbe-html :preparation-function my-blog-pages-preprocessor :completion-function my-blog-pages-postprocessor :htmlized-source t :headline-level 4 :section-numbers nil :with-toc nil :with-drawers t :with-sub-superscript nil ;; important!! ;; the following removes extra headers from HTML output -- important! :html-link-home "/" :html-head ,website-head :html-head-include-default-style nil :html-head-include-scripts nil :html-format-drawer-function my-blog-org-export-format-drawer :html-home/up-format "" :html-mathjax-options ,my-blog-local-mathjax :html-mathjax-template ,(concat my-blog-extra-mathjax-config "<script type=\"text/javascript\" src=\"%PATH\"></script>") :html-link-up "" :html-link-home "" :html-preamble website-header :html-postamble website-footer) ("blog-rss" :base-directory ,my-website-blog-dir :base-extension "org" :publishing-directory ,my-website-out-dir :publishing-function org-rss-publish-to-rss :with-author t :html-link-home "https://ogbe.net/" :html-link-use-abs-url t :title "Dennis Ogbe" :rss-image-url "https://ogbe.net/img/feed-icon-28x28.png" :my-rss-stylesheet "/res/rss.xsl" ; custom style sheet :completion-function my-website-rss-postprocessor :section-numbers nil :exclude ".*" :include ("blog-feed.org") :table-of-contents nil) ("blog-css" ; see my-website-cssjs-files as example for how these are published :base-directory ,my-website-css-dir :base-extension ".*" :exclude ,(rx (* anything) (or "~" "#" ".in")) :publishing-directory ,(concat my-website-out-dir "res") :publishing-function org-publish-attachment :preparation-function my-website-process-cssjs :completion-function my-website-clean-cssjs :recursive t) ("blog-images" :base-directory ,my-website-img-dir :base-extension ".*" :publishing-directory ,(concat my-website-out-dir "img") :publishing-function org-publish-attachment :recursive t) ("blog-dl" :base-directory ,my-website-dl-dir :base-extension ".*" :publishing-directory ,(concat my-website-out-dir "dl") :publishing-function org-publish-attachment :recursive t)))
Update here for an explanation):
: I changed a few minor things, including that I now finally add the publish date into the actual published blog post. Also, thanks to a hint by G.M., who provided a fix for my old structure-template definition, I now have an updated structure template to generate the header for a blog post (see(require 'org-tempo) (tempo-define-template "blog-header" ; just some name for the template '("#+title: ?" n "#+AUTHOR: Dennis Ogbe" n "#+EMAIL: [email protected]" n "#+DATE:" n "#+STARTUP: showall" n "#+STARTUP: inlineimages" n "#+BEGIN_PREVIEW" n p n "#+END_PREVIEW") "<b" "Insert blog header" ; documentation 'org-tempo-tags)