UP | HOME

Elisp Org-Publish Configuration

Table of Contents

Before getting in greater depth: Huge thanks to an extraordinary member of the Emacs community, whose (live) videos on YouTube, greatly helped me when I decided to… turn evil: David Wilson of System Crafters

In fact, this configuration is fully based on the one he presented on his YouTube channel.

;; build-site.el -- Summary

;;; WARNING: DO NOT EDIT THIS FILE. The elisp file should not be edited. Visit
;;; content/posts/20221228_elisp-org-publish-blog-configuration.org instead

;;; This is a configuration file for emacs to produce html code for my personal
;;; website from plain org mode files. To achieve this I utilize the included
;;; `ox-publish' package as well as `htmlize' (even though I can not understand
;;; why it does not behave as I would like it to ) and parts of
;;; `org-static-blog'

;;; WARNING: DO NOT EDIT THIS FILE. The elisp file should not be edited. Visit
;;; 20221228_elisp-org-publish-blog-configuration.org instead
;;; Yup, it is needed twice to make sure I'll read it.

;;; Commentary:
;;;; TODO Someday I will write something here. Till then ;)


;;;; I changed/restructured quite some stuff after having a look at:
;;;; https://her.esy.fun/posts/0001-new-blog/index.html

;;; Code:
;;;; Only to stop flycheck 

Getting the gist

This is a literate configuration file, setting up and emacs instance to be run on a simple virtual machine (Github actions). As such, you will not see most of the options one cares for when setting up emacs. If you look at my shell script, you can see that I run emacs as cleanly as possible:

emacs -Q --script build-site.el

Site-wide Variables

In this section of the configuration file I initialize the variables that I use in the rest of the script. The goal here is to have no hard coded paths and/or settings, and instead, rely on the modification of predefined variables to change the behavior of already implemented features.

(defvar domainname "https://blog.chatziiola.live"
  "Self-Descriptive. It is the address for which we build our site")

Content directories

To keep it simple:

(defvar base-dir "./content/"
  "The content directory.")

(defvar public-dir "./public/"
  "The root directory of our webserver.")

(defvar drafts-dir (concat base-dir "drafts/")
  "To be ignored when publishing.")

(defvar posts-dir (expand-file-name "posts/" base-dir)
  "Subfolder of content where posts lie.")

(defvar posts-public-dir (expand-file-name "posts/" public-dir)
  "The public subfolder in which posts will be published.")

(defvar src-dir "./content/src/"
  "Self-descriptive.")

(defvar src-public-dir "./public/src/"
  "Self-descriptive.")

CSS Path

(defvar css-path "/src/rougier.css"
  "Self-descriptive.")

Head HTML

(defvar org-blog-head
  (concat
   "<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">"
   "<link href=\"https://fonts.googleapis.com/css2?family=Fira+Sans&display=swap\"rel=\"stylesheet\">"
   "<link href=\"https://fonts.googleapis.com/css2?family=Roboto+Condensed&display=swap\"rel=\"stylesheet\">"
   "<link rel=\"stylesheet\" href=\"" css-path "\" />
    <link rel=\"icon\" type=\"image/x-icon\" href=\"/src/favicon.ico\">"
   "<meta charset=\"UTF-8\" name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
   )
  "Description - BLOG HTML HEAD.")

Postamble

(defvar general-postamble
  "<p class=\"footer\"> Made with Emacs and Org.<br>CSS theme based on the one developed by <a href=\"https://github.com/rougier\">@rougier</a>.</p>"
  "To be used on all pages.")

(defvar comments-postamble
  (concat
   "<script src=\"https://giscus.app/client.js\" data-repo=\"chatziiola/chatziiola.github.io\" data-repo-id=\"R_kgDOGq8p0g\" data-category=\"Announcements\" data-category-id=\"DIC_kwDOGq8p0s4COSFW\" data-mapping=\"pathname\" data-reactions-enabled=\"1\" data-emit-metadata=\"0\" data-input-position=\"bottom\" data-theme=\"light\" data-lang=\"en\" data-loading=\"lazy\" crossorigin=\"anonymous\" async> </script>"
   "<p class=\"date footer\"> Originally created on %d </p>"
   general-postamble)
  "Postamble for posts so that giscus comments are enabled.")

Org-static-blog index variables

Being the person that I am, striving for simplicity and creating chaos to get there, I have taken parts of this wonderful package org-static-blog, and butchered it to my needs

;;;; These were set up on a need-to-set basis
(setq org-static-blog-enable-tags t)
(setq org-static-blog-index-file "recents.html")
(setq org-static-blog-index-front-matter "")
(setq org-static-blog-index-length 50)
(setq org-static-blog-posts-directory "./content/posts/")
(setq org-static-blog-page-postamble general-postamble)
(setq org-static-blog-publish-directory "./public/posts/")
(setq org-static-blog-publish-title "Recent Articles")
(setq org-static-blog-publish-url "https://chatziiola.github.io")

Package Settings   restructure

These are pretty basic settings, only required in order for us to call a clean emacs instance. A separate directory for packages is specified in order to not liter our actual Emacs directory.

While one could avoid using extra packages ( as I have tried doing ) and keep it as simple as possible, it is not smart to reinvent the wheel. If the desired functionality is provided by a third party package, use it.

(setq make-backup-files nil
      auto-save-default nil
      create-lockfiles nil)

(require 'package)
(setq package-archives '(("melpa" . "https://melpa.org/packages/")
                         ("elpa" . "https://elpa.gnu.org/packages/")))

(setq user-emacs-directory (expand-file-name "./.packages"))
(setq package-user-dir user-emacs-directory)

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

Loading emacs does not mean that the publishing functions and variables have already been loaded. The following lines ensures that we have successfully loaded the actual publishing system

;; Load the publishing system
(require 'ox-publish)
(require 'ox-html)
(require 'cl-extra)

Installing use-package

;; Install dependencies
;; htmlize is needed for proper code formatting:
;; https://stackoverflow.com/questions/24082430/org-mode-no-syntax-highlighting-in-exported-html-page
(eval-when-compile
  (add-to-list 'load-path (expand-file-name "use-package" default-directory))
  (require 'use-package))

(use-package htmlize)

Org To Html Settings

These are settings that are used during the conversion of my org files (articles) to html files. Under certain circumstances they can be overwritten from the org-projects-alist’ options.

(setq org-src-fontify-natively t)
(setq org-html-htmlize-output-type 'css)

(setq org-src-fontify-natively t		; Fontify code in code blocks.
      org-adapt-indentation nil			; Adaptive indentation
      org-src-tab-acts-natively t		; Tab acts as in source editing
      org-confirm-babel-evaluate nil		; No confirmation before executing code
      org-edit-src-content-indentation 2	; No relative indentation for code blocks
      org-fontify-whole-block-delimiter-line t) ; Fontify whole block


;; Customize the HTML output
(setq org-html-validation-link nil
      org-html-head-include-scripts nil
      org-html-head-include-default-style nil
      org-html-indent nil
      org-html-self-link-headlines t
      org-export-with-tags t
      org-export-with-smart-quotes t
      org-html-head org-blog-head)

Babel

Babel-related settings. I’m pretty sure I had this enabled for a fancier feature than simply highlighting but I’m not 100% sure.

(org-babel-do-load-languages
 'org-babel-load-languages
 '((emacs-lisp . t)
   (gnuplot . t)
   (haskell . nil)
   (latex . t)
   (octave . t)
   (python . t)
   (matlab . t)
   (shell . t)
   (ruby . t)
   (sql . nil)
   (sqlite . t)))

Series next/before links   INACTIVE

This is a functionality that I long wanted to have implemented. Now, although this works perfectly, it creates a restriction: The files are chronologically inserted in the previous/next “queue”. This means that in the case of lectures, which I have irregularly published at times, there may be a slight confusion. To avoid this causing a problem we must be careful with the dates in the filenames of forced lectures.

(defun my-find-next-previous-series-article (file)
  "Find the filenames of the next and previous article, if they exist, in the same directory as FILE.
   This function acts only on lectures (files starting with 'lec_.) as of now.
The filenames are returned in HTML format."
  (when (string-prefix-p "lec_" (file-name-nondirectory file))
    (let* ((dir (file-name-directory file))
           (name (file-name-nondirectory file))
           (files (sort (directory-files dir nil "^lec_.*\\.org$") 'string<))
           (index (cl-position name files :test 'equal)))

        ;; This is a hack to ensure that the files get returned with the .html extension
        (let ((prev (if (and (> index 0) (nth (1- index) files))
                             (concat (file-name-sans-extension (nth (1- index) files)) ".html")))
              (next 
                    (if (and (< index (1- (length files))) (nth (1+ index) files))
                        (concat (file-name-sans-extension (nth (1+ index) files)) ".html"))))
          (cons prev next))))
  )

This function works really well the preceding one yet is not ideal. It inserts the content at the end of the org mode buffer, causing problems with footnotes, where they exist.

(defun my-add-links-to-next-previous-series-article (backend)
  "Add links to the previous and next series articles, if they exist."
  (when (org-export-derived-backend-p backend 'html)
    (let ((prev-next (my-find-next-previous-series-article (buffer-file-name))))
      (when prev-next
        (let ((prev (car prev-next))
              (next (cdr prev-next)))
          (when (or prev next)
            (goto-char (point-max))
            (forward-line 1)
            (insert "\n#+begin_export html\n")
            (insert "<div class=\"series-navigation-div\">\n")
            (when prev (insert (format "<p><a class=\"nav-button previous-nav-button\" href=\"%s\">Previous</a></p>\n" prev)))
            (when next (insert (format "<p><a class=\"nav-button next-nav-button\" href=\"%s\">Next</a></p>\n" next)))
            (insert "</div>\n")
            (insert "#+end_export")))))))

(add-hook 'org-export-before-parsing-hook 'my-add-links-to-next-previous-series-article)

The perfect solution would be one like the preceding, the only problem with this one is that it no longer works with the find-previous-next algorithm, since we do not know which html files are created (only the previous one exists, the we can not add a next button).

(defun my-add-links-to-next-previous-series-article ()
  "Add links to the previous and next series  articles, if they exist."
  (let ((prev-next (my-find-next-previous-series-article (buffer-file-name))))
    (when prev-next
      (let ((prev (car prev-next))
            (next (cdr prev-next)))
        (goto-char (point-max))
        (unless (re-search-backward "<div id=\"postamble\"" nil t)
          (error "Could not find postamble div"))
        (insert "\n<div class=\"series-navigation-div\">\n")
        (when prev (insert (format "<p><a class=\"nav-button previous-nav-button\" href=\"%s\">Previous</a></p>\n" prev)))
        (when next (insert (format "<p><a class=\"nav-button next-nav-button\" href=\"%s\">Next</a></p>\n" next)))
        (insert "</div>\n")
        ))))

(add-hook 'org-export-after-parsing-hook #'my-add-links-to-next-previous-series-article)

Tips:

One could use relative paths (even though I now (<2023-01-01 Sun>) find some problems with this approach, as it breaks some stuff when creating index pages), to ensure that no faulty links exist.

Project Alist

Org publishing works with projects, a fancy way to call files and directories.

There are four projects, with different variables and settings:

  1. Org-files, all org files in my /content folder
  2. Blog-posts, all org files in my /content/posts folder
  3. Images, images in /content/images
  4. Static, html and css files in /content/src
(setq org-publish-project-alist
      (list

The Brief

The org mode files ( the articles ) exist in the /contents/ folder, which is where I’m working. We want to automate the process of converting these files to html and moving this web-friendly version to the /public directory, which is the root directory of our web server.

To achieve that we first convert all org mode files

Summing up

  1. Order is crucial, since the export is sequential and the later exports may overwrite previous ones.

Org-files

(list "org-files"

It contains all files in /content/ except for my draft articles. ( It should work like that, however drafts are currently being exported… Maybe someone notices the error here and proposes a solution in the comments )

:base-directory base-dir
:base-extension "org"
:exclude drafts-dir
:recursive t

Kinda general, the publishing function and where to publish

:html-link-home "/index.html"
:html-link-up "../index.html"
:html-postamble general-postamble
:publishing-directory public-dir
:publishing-function 'org-html-publish-to-html
:with-author nil           ;; Don't include author name
:with-creator nil            ;; Include Emacs and Org versions in footer
:with-drawers t
:headline-level 4

Table of contents has been taken offline due to the fact that I did not like how it looked.

:with-toc nil

Section numbers do not work with my css since it provides numbering.

:section-numbers nil       ;; Don't include section numbers

This is a setting that gets overwritten for blog posts but it essentially makes the home button to point to the home page of my website and the up button to go a directory higher.

:html-link-home "/index.html"
:html-link-up "../index.html"

This could be a rather useful entry, if there was maybe an integration with version control so that files would only get published if the had been edited. The problem is that it makes all posts have the same date and that does not look nice. A better way to deal with this problem is the #+DATE: header at the top of blog posts.

:time-stamp-file nil)

Blog-posts

(list "blog-posts"

This is crucial.

:base-directory posts-dir
:base-extension "org"
:exclude ".*index.org"

Recursive once again

:recursive t

Another difference

:html-link-up "./index.html"
:html-link-home "/index.html"

Yup, I decided against that

;     :auto-sitemap t
;     :sitemap-filename "sitemap.org"
;     :sitemap-title "Sitemap"
;     :sitemap-sort-files 'anti-chronologically
;     :sitemap-date-format "Published: %a %b %d %Y"
:html-postamble  comments-postamble
:publishing-directory posts-public-dir
:publishing-function 'org-html-publish-to-html

The following settings actually do not need further explanation

:with-author t           ;; Don't include author name
:with-creator t            ;; Include Emacs and Org versions in footer
:with-drawers t
:with-date t
:headline-level 4
:with-toc t                ;; Include a table of contents
:section-numbers nil       ;; Don't include section numbers
:time-stamp-file nil)

Images

You may have already noticed that the two previous projects contain only my org files, even though a website is so much more than html ( to which org will get converted ). There are other types of media, such as images and css elements. In order to get these published ( contained in /content/images and /content/src respectively) we use the org-publish-attachment function, which essentially copies the specified files to the target directory

(list "Images"
      :base-directory posts-dir
      :base-extension "png"
      :publishing-directory posts-public-dir
      :publishing-function 'org-blog-publish-attachment
      :recursive t
      )

Static

(list "Website static stuff"
      :base-directory src-dir
      :base-extension "html\\|css\\|ico"
      :publishing-directory src-public-dir
      :publishing-function 'org-publish-attachment
      :recursive t
      )
)
)



And another function to help with images

;; Automatic image conversion
(defun org-blog-publish-attachment (plist filename pub-dir)
  "Publish a file with no transformation of any kind.
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.
Take care of minimizing the pictures using imagemagick.
Return output file name."
  (unless (file-directory-p pub-dir)
    (make-directory pub-dir t))
  (or (equal (expand-file-name (file-name-directory filename))
             (file-name-as-directory (expand-file-name pub-dir)))
      (let ((dst-file (expand-file-name (file-name-nondirectory filename) pub-dir)))
        (if (string-match-p ".*\\.\\(png\\|jpg\\|gif\\)$" filename)
            (shell-command (format "convert %s -resize 800x800\\> +dither -colors 16 -depth 4 %s" filename dst-file))
          (copy-file filename dst-file t)))))

The End - Taking Action

(org-publish-all t)

Org-static-blog for index creation

I tried removing this section, but in great terror I realized how naive I was when I wrote this. It needs the variables that I set in here.

(load (expand-file-name "index-generator.el" default-directory))
(chatziiola/org-static-blog-assemble-index-no-content)

;;; build-site.el ends here.

Sources of inspiration

This is intended to be the last section of my literate config file. It is devoted to all the websites that inspired me to take action towards improving my site:

Originally created on 2022-07-09 Sat 00:00