How to publish Emacs org files as xhtml

Table of Contents

Everything should be made as simple as possible, but not any simpler – Albert Einstein

Introduction

Org-mode is an powerful plian text authoring system in Emacs with uniqe tailored for literate programming and reproducible research. Built on its markup language, Org-mode is ideal for note-taking, maintaining TODO lists, and orchestrating project planning. Furthermore, org-mode servers as a primary authoring system, boasting capabilities to export its '.org' files into various formats including HTML, LaTeX, Open Document, Markdown, among others. The standout features of org-mode include:

  • Elegant Markup
  • Structured Editing
  • Superior Source Code Integration
  • Comprehensive Tasks Management
  • Seamless Data Capture from Multiple Sources
  • Versatile Export and Publishing Options
  • Extremely Extensible

To leverage the universal nature of plain text and to promote innovative contents sharing, this project illustrates how to automatically export Org files as XHTML, crafting a visually appealing static website hosted on GitHub.

First things first: Sharpen tools before diving into work

For this project, the essential tools required are Emacs (V28.2) with org-mode (V9.5.3) and a web broswer with a development mode, specifically Google Chrome.

The .emacs.d could be found: https://github.com/Ethanlinyf/General-Pure-Emacs

The basic foler structure for this project

The files and folders are orgnised as follows:

~/Org-site/
                |- public/
                |     |- .git/
                |     |- img/
                |     |- theme/
                |          |- CSS/
                |          |- js/
                |     |- index.html
                |     |- nav.html
                |     |- blog.html
                |- source/
                |     |- .git/
                |     |- img/
                |     |- theme/
                |          |- CSS/
                |          |- js/
                |          |- setups
                |     |- org/
                |        |- index.org
                |        |- nav.org
                |        |- blog.org
                |- backup
              

Available and maintained through Git and GitHub

Maintance by Git and remote repository at GitHub

Version control by git for the folder ~/Org-site and create a private repository (ethanlinyf.github.io) in my GitHub (Ethanlinyf). Then synchronise the two subfolders to two branches:

  • In the folder, ~/Org-site/public/

    echo "* Public Website" >> README.org
                          git init
                          git add README.org
                          git commit -m "first commit"
                          git branch -M main
                          git remote add origin git@github.com:Ethanlinyf/ethanlinyf.github.io
                          git push -u origin main
                        
  • In the folder, ~/Org-site/source/

    echo "* source" >> README.org
                          git init
                          git add README.org
                          git commit -m "first commit"
                          git branch -M source
                          git remote add origin git@github.com:Ethanlinyf/ethanlinyf.github.io
                          git push -u origin source
                        

The following maintance for this project folder, we just need three steps for the two folders mapping to the two branches respectively: public & source:

git add .
                  git commit -m "<commit message>"
                  git push
                

Published through GitHub to make sharing available

GitHub Pages is available in public repositories with GitHub Free and GitHub Free for organizations, and in public and private repositories with GitHub Pro, GitHub Team, GitHub Enterprise Cloud, and GitHub Enterprise Server.

  • register a domaian name A fency domain name is needed to replace the default one (ethanlinyf.github.io). In this project, "thethingsengion.org" is registered for this web service.
  • GitHub Pages for web service GitHub Pages is designed to host this project pages from the GitHub repository (ethanlinyf.github.io). customDomain.png
    • Source
      This GitHub Pages site is currently being built from the main branch related to the public folder of the project folder structure, which is exported to be made public. websiteSource.png
    • Theme chooser
      These css and js files were provided and maintained in this project. So, we needn't to specify a theme to publish this site with a Jekyll theme. themeChooser.png
    • Enforce HTTPS
      With a layer of encryption to prevents others from snooping on or tampering with traffic to this website, HTTPS is enable for this project. websitHTTPS.png

Configuration of the org publishing project on Emacs

To specify publishing behaviours from org to html, we have to setup the variable of org-publish-project-alist, which is defined in 'ox-publishing.el'.

There are two major components to publish org to xthml and one extra can be used to publish all with one command:

  • The source components
  • The static components
  • The publish components

These three components will be included in the following configuration structure:

(require 'ox-publish)
              (setq org-publish-project-alist
              '(

              ;; The three components here

              ))
            

Settings for source components

In this setting, a publishing function "org-html-publish-to-html" to publish dynamic contents, wich refers to note-taken org files converted to xhtml files for publishing. The most important settings are as follows: The most important settings here are:

base-directory The components root directory
base-extension Filename suffix without the dot
publishing-directory The base directory where all our files will be published
recursive If t, include subdirectories
publishing-function How org should process the files in this component

The html head could also be included, as well as the validation link. Other defualt settings, such as with-toc, headline-levels, can be specified by the export options templates, which will be further discussed.

("orgfiles"
                ;; source and public for this project.
                :base-directory "~/Org-site/source/org/" ;; the source org folder to export html files
                :publishing-directory "~/Org-site/public/" ;; publishing-directory for this project 
                ;; :preparation-function
                ;; :complete-function
                :base-extension "org"
                ;; :exclude "PrivatePage.org"     ;; regexp supported
                ;; :include
                :recursive t
                ;:auto-sitemap t
                ;:sitemap-filename "sitemap.org"
                ;:sitemap-title "Sitemap"
                ;:sitemap-sort-folders last           

                ;;Publishing behaviours
                :publishing-function org-html-publish-to-html

                ;; Generic properties for this project
                :headline-levels 4    ;; org-export-headline-levels
                :language "en"        ;; org-export-default-language
                :section-numbers nil  ;; org-export-with-section-numbers
                :with-planning t      ;; org-export-with-planning
                :with-priority t      ;; org-export-with-priority ;
                ;;  :with-tags not-in-toc ;; org-export-with-tags
                :with-toc t           ;; org-export-with-toc

                :html-doctype "html5" ;; org-html-doctype
                ;;  :html-metadata-timestamp-format "%Y-%m-%d" ;; org-html-metadata-timestamp-format
                :html-head-include-default-style nil ;; org-html-head-include-default-style
                :html-head-include-scripts nil ;; org-html-head-include-scripts
                ;; :html-head
                ;; "<link rel=\"shortcut icon\" href=\"themes/assets/icon.png\" type=\"img/x-icon\" />
                ;; <link rel=\"stylesheet\" href=\"themes/style.css\" type=\"text/css\"  />
                ;; <script type=\"module\" src=\"themes/main.js\" defer></script>" ;; org-html-head
                :html-checkbox-type unicode  ;; org-html-checkbox-type
                :html-indent t               ;; org-html-indent
                ;; :html-link-home "index.html"	;; org-html-link-home
                ;; :html-link-up "uUP"          ;; org-html-link-up
                :html-validation-link "<a href=\"http://www.thingsengine.org/\">ThingsEngine</a>"    ;; org-html-validation-link
                )
              

Settings for static components

Static components include necessary files supporting to publish the coverted source component. Some files are css/js files and meida files. So, the needed funciton of publishing-function for this static component is "org-publish-attachment", just copying from :base-directory to :publishing-directory without changing them.

;; static assets
                ("org-site"
                :base-directory "~/Org-site/source/"
                :base-extension "js"
                :publishing-directory "~/Org-site/public/"
                :recursive nil
                :publishing-function org-publish-attachment
                )
                ("images"
                :base-directory "~/Org-site/source/img"
                :base-extension any
                :publishing-directory "~/Org-site/public/img"
                :recursive t
                :publishing-function org-publish-attachment
                )
                ("themes"
                :base-directory "~/Org-site/source/theme/"
                :base-extension any
                :publishing-directory "~/Org-site/public/theme/"
                :recursive t
                :publishing-function org-publish-attachment
                )

                ;; reverse static assets
                ("rorg-site"
                :base-directory "~/Org-site/public/"
                :base-extension "js"
                :publishing-directory "~/Org-site/source/"
                :recursive nil
                :publishing-function org-publish-attachment
                )
                ("rimages"
                :base-directory "~/Org-site/public/img/"
                :base-extension any
                :publishing-directory "~/Org-site/source/img/"
                :recursive t
                :publishing-function org-publish-attachment
                )
                ("rthemes"
                :base-directory "~/Org-site/public/theme/"
                :base-extension any
                :publishing-directory "~/Org-site/source/theme/"
                :recursive t
                :publishing-function org-publish-attachment
                )
              

Settings for publish components

You may want to publish all with just one command, which can be set in the publish components with suffixes and basenames of the project.

("website" :components ("orgfiles" "org-site" "images" "themes"))
                ("statics" :components ("confs" "images" "themes"))
                ("rstatics" :components ("rorg-site" "rimages" "rthemes"))
              

Then, publish everything with M-x org-publish-project RET org RET recursively to ~/Org-site/public/.

Define directory-local variables for this project

To avoid pullute the export engine,the above settings can be redifined for this project in the certain directory "~/Org-site/source" and its subdirectories. See the following examples:

((org-mode . (
                (org-export-in-background . t)
                (org-html-htmlize-output-type . inline-css)
                (org-html-head-include-default-style . nil)
                (org-html-head-include-scripts . nil)
                (org-publish-project-alist . (("orgfiles"
                :base-directory "~/Org-site/source/org/" ;; the source org folder to export 
                )

                ;; static assets
                ("org-site"
                :base-directory "~/Org-site/source/"
                :base-extension "js"
                :publishing-directory "~/Org-site/public/"
                :recursive nil
                :publishing-function org-publish-attachment
                )

                ("rorg-site"
                :base-directory "~/Org-site/public/"
                :base-extension "js"
                :publishing-directory "~/Org-site/source/"
                :recursive nil
                :publishing-function org-publish-attachment
                )

                ("website" :components ("orgfiles" "org-site" "images" "themes"))
                ))
                (eval .
                (progn
                (defun save-and-publish-website()
                "Save all buffers and publish."
                (interactive)
                (when (yes-or-no-p "Really save and publish current project?")
                (save-some-buffers t)
                (org-publish-project "website" t)
                (message "Site published done.")))

                (define-minor-mode auto-save-and-publish-file-mode
                "Toggle auto save and publish current file."
                :global nil
                :lighter ""
                (if auto-save-and-publish-file-mode
                ;; When the mode is enabled
                (progn
                (add-hook 'after-save-hook #'save-and-publish-file :append :local))
                ;; When the mode is disabled
                (remove-hook 'after-save-hook #'save-and-publish-file :local)))

                (add-hook 'org-mode-hook #'auto-save-and-publish-file-mode))))))
              

See more details of this settings in the GitHub repository, "General-Pure-Emacs".

To publish

Needed functions to facilitate the publishing

To improve the efficiency, more functions are define to facilitate the publishing.

(defun save-and-publish-statics ()
                "Just copy statics like js, css, and image file .etc."
                (interactive)
                (org-publish-project "statics" t)
                (message "Copy statics done."))

                (defun save-and-publish-rstatics ()
                "Just copy statics like js, css, and image file .etc.
                Which is a reverse operation of `save-and-publish-statics'."
                (interactive)
                (org-publish-project "rstatics" t)
                (message "Copy rstatics done."))

                (defun save-and-publish-file ()
                "Save current buffer and publish."
                (interactive)
                (save-buffer t)
                (org-publish-current-file t))

                (defun delete-org-and-html ()
                "Delete current org and the relative html when it exists."
                (interactive)
                (when (yes-or-no-p "Really delete current org and the relative html?")

                (let ((fileurl (concat "~/site/public/" (file-name-base (buffer-name)) ".html")))
                (if (file-exists-p fileurl)
                (delete-file fileurl))
                (delete-file (buffer-file-name))
                (kill-this-buffer)
                (message "Delete org and the relative html done."))))

                (defun just-delete-relative-html ()
                "Just delete the relative html when it exists."
                (interactive)
                (when (yes-or-no-p "Really delete the relative html?")

                (let ((fileurl (concat "~/site/public/" (file-name-base (buffer-name)) ".html")))
                (if (file-exists-p fileurl)
                (progn
                (delete-file fileurl)
                (message "Delete the relative html done.")
                )
                (message "None relative html.")))))

                (defun preview-current-buffer-in-browser ()
                "Open current buffer as html."
                (interactive)
                (let ((fileurl (concat "http://127.0.0.1:8080/" (file-name-base (buffer-name)) ".html")))
                (save-and-publish-file)
                (unless (httpd-running-p) (httpd-start))
                (browse-url fileurl)))
              

Localised testing

In Emacs, use the package "simple-httpd"

(use-package simple-httpd
                  :ensure t
                  :config
                  (setq httpd-root "~/Org-site/public"))
                

And/Or, in the host system, use "Live Server"

This is a little development server with live reload capability. Use it for hacking your HTML/JavaScript/CSS files, but not for deploying the final site.

There are two reasons for using this:

  1. AJAX requests don't work with the file:// protocol due to security restrictions, i.e. you need a server if your site fetches content through JavaScript.
  2. Having the page reload automatically after changes to files can accelerate development.

You don't need to install any browser plugins or manually add code snippets to your pages for the reload functionality to work, see "How it works" sectionbelow for more information. If you don't want/need the live reload, you shouldprobably use something even simpler, like the following Python-based one-liner:

  • Installation

    python -m SimpleHTTPServer
                        

    You could manually install this server software with the following steps:

    git clone https://github.com/tapio/live-server
                          cd live-server
                          npm install # Local dependencies if you want to hack
                          npm install -g # Install globally
                        
  • How to use it
    Issue the command live-server in your project's directory. Alternatively you can add the path to serve as a command line parameter.

    This will automatically launch the default browser. When you make a change to any file, the browser will reload the page - unless it was a CSS file in which case the changes are applied without a reload.

    If a file ~/.live-server.json exists it will be loaded and used as default options for live-server on the command line. See "Usage from node" for option names.

    var liveServer = require("live-server");
    
                          var params = {
                          port: 8181, // Set the server port. Defaults to 8080.
                          host: "0.0.0.0", // Set the address to bind to. Defaults to 0.0.0.0 or process.env.IP.
                          root: "/public", // Set root directory that's being served. Defaults to cwd.
                          open: false, // When false, it won't load your browser by default.
                          ignore: 'scss,my/templates', // comma-separated string for paths to ignore
                          file: "index.html", // When set, serve this file (server root relative) for every 404 (useful for single-page applications)
                          wait: 1000, // Waits for all changes, before reloading. Defaults to 0 sec.
                          mount: [['/components', './node_modules']], // Mount a directory to a route.
                          logLevel: 2, // 0 = errors only, 1 = some, 2 = lots
                          middleware: [function(req, res, next) { next(); }] // Takes an array of Connect-compatible middleware that are injected into the server middleware stack
                          };
                          liveServer.start(params);
                        

Manage by Git and publish through GitHub

After completing the previous settings, we are now set to use the established workshop to export org-mode files to XHTML files. The further development could be included as follows:

  • Optimise the recent theme for the websit
  • Configuring default settings for various org templates
  • How to manage project files including initial org file through git and GitHub

For managing both project and websit files, we can utilise the tool "Git" and the web service "GitHub" to facilitate converting the Org files to XHTML files while maintaining version control. A repository can be crated with two branches: the "main" could be used for publishing XHTML files, and the "source" branch to can serve to house the source files including the websit assets (such as themes, images, setups and so on) and the initial Org files.

The typical workflow can be illustrated in the following diagram:

org2xhtml.png

Figure 1: Org2XHTML Workflow

Reference

Date: 2021-07-27 Tue 00:00

Author: Dr YF Lin

Created: 2023-08-11 Fri 19:21

ThingsEngine