Logo

How I use org-publish to create my blog / website

Table of Contents

This website was made pretty much exclusively by using the publish functionality provided by org-mode. Here's how I did it:

1. Directory Structure

I keep a git repository for easy and version control. Let's have a look at it!

tree -d ..
..
├── build
├── dist
│   ├── assets
│   │   └── img
│   └── blog
└── src
    └── assets
        └── img

9 directories

The build directory is where all of the files necessary for building the website go and dist is where all of the files go after they were exported and compiled by org-publish. It's not included in git commits (part of .gitignore).

src is where all of my source content files live. Let's have a more detailed look at it:

tree .
.
├── about.org
├── assets
│   └── img
│       └── logo.jpg
├── hello-world.org
├── index.org
├── links.org
├── my-home-network-backup-architecture.org
├── org-publish-setup.org
└── this-website-is-ugly.org

3 directories, 8 files

As you can see all of the pages are individual .org files, which simply reside in the source directory. index.org is generated automatically by org-publish.

Files in the assets directory are not compiled to HTML by org-publish, but simply copied over (spoiler: you can achieve this by using org-publish-attachment instead of org-html-publish-to-html as your publishing function).

2. Build files

Apart from my Makefile (which resides in the project root), all of my build files are (who would've guessed it) in the build directory. Here's its contents:

tree ../build
../build
├── export.el
├── export.org
├── postamble.html
└── preamble.html

1 directory, 4 files

First, let's take a closer look at my export.org file that defines how org bundles the export from .org files to HTML. I have taken a more literate approach with this file so I hope it is obvious from the raw file contents alone what's being done.

In the next paragraph, it will become obvious how I generate export.el from this file using org-babel.

2.1. export.org

First, I need to require the ox-publish package which provides org-publish support

(require 'ox-publish)

Because I can't use doom to compile, but have to use the base emacs command, I will now load the babel packages:

(org-babel-do-load-languages
 'org-babel-load-languages
 '((emacs-lisp . t)
   (shell . t)))

Next, I want all files to be recompiled when I build the site, not only those who changed. If I don't do this, I think changes to my header and footer won't be applied globally unless I edit a file.

(setq org-publish-use-timestamps-flag nil)

I think it looks ugly if I include raw HTML strings in my publish project list, so I want to define two 'components' that contain pre- and postamble respectively.

This code was generated using ChatGPT. I think it opens a temporary buffer, inserts the file contents and then returns them as a string into the variable. Sounds quite complicated, so maybe there's a smoother way without opening a temporary buffer.

(setq my-preamble
      (with-temp-buffer
        (insert-file-contents "./preamble.html")
        (buffer-string)))

(setq my-postamble
      (with-temp-buffer
        (insert-file-contents "./postamble.html")
        (buffer-string)))

Now I'm ready to define my org-publish 'projects' (which are really collections for related content as far as I can tell)

(setq org-publish-project-alist

To properly include my header / footer components. I need to use this backtick instead of the simple tick and then tell org-publish to only take fetch these two variable values using ","

`(("page"
   :html-doctype "html4-strict"
   :base-directory "../src/"
   :publishing-directory "../dist/"

only include org files

:base-extension "org"
:publishing-function org-html-publish-to-html

load header / footer html files from build directory

:html-preamble ,my-preamble
:html-postamble ,my-postamble
:html-checkbox-type unicode

generate sitemap which is used as index

:auto-sitemap t

and rename it to the default entrypoint index.html

:sitemap-filename "index.org"
:sitemap-title "Articles"
:sitemap-sort-files anti-chronologically
:sitemap-style list

strip css (or not I guess)

;; :html-head-include-default-style nil

include my data

 :author "r1bb0n"
 :email "technologika@posteo.net"
 :with-email t
)

Now come the static assets:

("static"
 :base-directory "../src/assets"
 :publishing-directory "../dist/assets"
 :base-extension "css\\|js\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|ogg\\|swf"
 :recursive t

use this function to just copy over files to dist

 :publishing-function org-publish-attachment
)

include all components in complete website build

     ("website" :components ("page" "static")))
)

To remove CSS from code blocks, I disable htmlize

(setq org-html-htmlize-output-type nil )

2.2. Pre- & Postamble templates

In addition to my main export file, I have separated pre- & postamble into html templates that are automatically inserted into, such that I don't have to write raw HTML in my elisp script.

3. Building and deploying the blog with make

All that's left to do now is to build & deploy the site at will. As I can call emacs without its GUI using the --batch flag, I make use of that and write a simple Makefile:

##
# Technologika Website
#
# @file
# @version 0.1

deploy: publish
        neocities-deploy deploy

publish: ./build/export.el
        emacs --batch ./build/export.el \
                --eval '(progn (setq org-export-with-broken-links t) (eval-buffer) (org-publish-project "website"))'

./build/export.el: ./build/export.org
        emacs --batch ./build/export.org -f org-babel-tangle

# end

The first target builds the blog by chaining together a few elisp functions:

  1. I had to enable broken org links because something got messed up when I transferred over to building with this.
  2. Then, I evaluate the whole configuration
  3. Finally, the html files are compiled / copied over for the whole project.

Afterwards, it is deployed using neocities-deploy.

The second target is a pre-requisite for the second. Since I use org-babel to tangle together my export script that I have written in literate style, it needs to be re-run and compose a new export.el when I change my export options.

Although it took a while to dial-in, I am super happy to see that everything seems to work just as I intended, so I guess it's time to deploy the website to Neocities…


Author: r1bb0n (technologika@posteo.net)

Page created: 2026-02-26 Do 11:52

Last update: 2026-02-27 Fr 20:56