Config Org-Mode For Publishing

2025-02-21, Fri

Org-Mode comes with exporting capability through a bunch of org-export-* functions. e.g. there is org-html-export-to-html that converts current buffer to an HTML file. However, these export functions typically handle a single file, which means when working with multiple files in a directory, it's org-publish-* functions that come to the rescue. When org-publish is invoked for the first time, it asks for a proper configuration of variable org-publish-project-alist to be made1, which is what this article is about.

Notes:

1. Quick Set Up

The Org-Mode manual provides a sample configuration2 that works with little tweak. Combined it with another example3, we now have:

(setq org-publish-project-alist
        `(("orgfiles"
           :base-extension "org"
           :exclude "PrivateFile.org"
           ;; :exclude ,(rx (or "PrivateFile.org" (seq line-start "private/"))) ;; regexp
           :base-directory "/path/to/org-src/"
           :publishing-directory "/path/to/org-publish/"
           :publishing-function org-html-publish-to-html
           ;; :headline-levels 3
           ;; :section-numbers nil
           :with-toc nil
           :html-head "<link rel=\"stylesheet\" href=\"/main-styles.css\" type=\"text/css\"/>"
           :html-preamble "<a href=\"/\">Home</a>"
           :html-postamble t
           :recursive t)
          ("assets"
           :base-directory "/path/to/org-src/"
           :publishing-directory "/path/to/org-publish"
           :base-extension "jpg\\|gif\\|png\\|css"
           :publishing-function org-publish-attachment
           :recursive t)
          ("website" :components ("orgfiles" "assets"))))

Notable properties here are:

  • :recursive scans all sub-directories
  • :html-head puts a stylesheet <link> element into <head>
  • :html-preamble puts a custom header element into <body>
  • :htmt-postamble puts a default footer element into <body>

Full list of other properties could be found in the Org-Mode manual4. They are also listed in ox-publish.el and ox-html.el.

2. Organize Configs

Configurations are like code – well, techically the are code here, so when duplications happen, it is time for refactoring. Out first step is to extract string literals into constants.

(setq my-src-dir "/path/to/org-src/"
        my-publish-dir "/path/to/org-publish/"
        my-html-head "<link rel=\"stylesheet\" href=\"/main-styles.css\" type=\"text/css\"/>"
        my-html-preamble "<a href=\"/\">Home</a>"
        my-html-postamble "<p class=\"author\">Author: %a (%e)</p>  <p class=\"date\">Date: %d</p>  <p class=\"creator\">%c</p>")

(setq org-publish-project-alist
        `(("orgfiles"
           :base-extension "org"
           :exclude "PrivateFile.org"
           :base-directory ,my-src-dir
           :publishing-directory ,my-publish-dir
           :publishing-function org-html-publish-to-html
           :with-toc nil
           :html-head ,my-html-head
           :html-preamble ,my-html-preamble
           :html-postamble ,my-html-postamble
           :recursive t)
          ("assets"
           :base-directory ,my-src-dir
           :publishing-directory ,my-publish-dir
           :base-extension "jpg\\|gif\\|png\\|css"
           :publishing-function org-publish-attachment
           :recursive t)
          ("website" :components ("orgfiles" "assets"))))

Even with new configurations available, org-publish still checks the source files' timestamp to decide whether to re-export them. According to the function doc: "When optional argument FORCE is non-nil, force publishing all files in PROJECT", hence invoke C-u before org-publish should trigger a fresh publish.

2.1. Handle index.org separately

The home page doesn't need a preamble that contains link to Home itself (or does it?). In order to customize this behavior, we could introduce a new project config to handle home page separately, e.g.

(setq org-publish-project-alist
            `(("index"
               :base-extension "org"
               :exclude "PrivateFile.org"
               :base-directory ,my-src-dir
               :publishing-directory ,my-publish-dir
               :publishing-function org-html-publish-to-html
               :with-toc nil
               :html-head ,my-html-head
               :html-preamble nil
               :html-postamble ,my-html-postamble)
              ("orgfiles"
               :base-extension "org"
               :exclude "\\(index\\|PrivateFile\\).org" ;; exclude index.org along with private files
               :base-directory ,my-src-dir
               :publishing-directory ,my-publish-dir
               :publishing-function org-html-publish-to-html
               :with-toc nil
               :html-head ,my-html-head
               :html-preamble ,my-html-preamble
               :html-postamble ,my-html-postamble
               :recursive t)
              ("assets"
               :base-directory ,my-src-dir
               :publishing-directory ,my-publish-dir
               :base-extension "jpg\\|gif\\|png\\|css"
               :publishing-function org-publish-attachment
               :recursive t)
              ("website" :components ("index" "orgfiles" "assets"))))

This seems like a candicate for macro, which will not be covered here. If you are caught up with the regular expression used in :exclude, try to test a few custom patterns on function string-match in the *scratch* buffer.

3. Common Tasks

This section includes solution for some common tasks.

  • Q: How to include JavaScript into a single HTML?

    A: Use the HTML_HEAD or HTML_HEAD_EXTRA keywords. See HTML Specific export settings5 for more info. Be careful that these page-level configurations could override project publish settings, as they may rely on the same variable.

    Table 1: Comparison of export and publish settings
    Variable Name Page-Level Keyword HTML Publish Keyword
    org-html-head #+HTML_HEAD :html-head
    org-html-head-extra #+HTML_HEAD_EXTRA :html-head-extra
  • Q: How to insert HTML tags into the output HTML?

    A: Use raw code blocks through #+HTML or #+BEGIN_EXPORT html. See Quoting HTML tags6 for more info.

4. What's Next

org-publish is defined in ox-publish.el, and org-html-export-to-html is defined in ox-html.el. The source code is well maintained and documented, so it shouldn't be too much trouble if further customization is needed.

Exercises:

  • Check when org-html-inner-template is invoked, and try to customize its behavior
  • Check when org-html-template is invoked, and try to customize its behavior
  • Instead of utilizing org-html-publish-to-html for HTML publication, try to come up with something like my-org-html-publish-to-html to designate the document structure and default style. Hint: start with the (org-export-define-backend 'html...) line in ox-html.el.

5. Revision History

Footnotes:

1

If it was some other GUI application, it might be the time when a "Wizard" modal dialog shows up that guides you step by step on how to completing the following process.

2

Org-Mode manual provides an example on complex publishing configuration: https://orgmode.org/manual/Complex-example.html. Similar content is also available on EmacsDocs.org through https://emacsdocs.org/docs/org/Complex-example.