Table of Contents
Disclaimer
I'm neither proficient in Org Mode (further on "Org"), nor a good front-end engineer. I think that a simple solution is better than no solution. If you see a mistake, you can contact me via iam@fidonode.me.
What is Org?
Your life in plain text
A GNU Emacs major mode for keeping notes, authoring documents, computational notebooks, literate programming, maintaining to-do lists, planning projects, and more — in a fast and effective plain text system.
Everything you can do in Org is to write a text. With a special markup, of course. This makes it versatile and extensible.
Why Org Mode?
- Plain text. Plain text as a data source offers significant versatility. You can read and understand what happens in org files without needing Emacs.
- Everything tool. Org Mode is a planner with an agenda, a to-do list, a note-taking app, a Jupyter Notebook-like tool, and a zettelkasten tool. You can manage almost every aspect of your life with Org Mode.
- The only way to eat an elephant is piece by piece. I do not have a habit of collecting and keeping information. I believe that discovering other aspects of Org Mode will lead me to better note-taking practices.
Render Org to blog or whatever
Org already has a way to render files into HTML, allowing you to create simple HTML files with minimal styling. I'm not interesting in styling from org, so I decide to use picocss framework.
Render HTML
I want to change some templates here and there. I've found esxml
package. It is a decent DSL for writing XML/HTML.
Here is how page header and footer look in this DSL.
(defun my/header (info) `(header (@ (class "header")) (nav (ul (li (strong ,(org-export-data (plist-get info :title) info)))) (ul (li (a (@ (href "/index.html")) "About")) (li (a (@ (href "/blog.html")) "Blog")) (li (a (@ (href "/rss.xml")) "RSS")) ) )) ) (defun my/footer (info) `(footer (@ (class "footer")) (hr) (p "Alex Mikhailov") (p "Built with: " (a (@ (href "https://www.gnu.org/software/emacs/")) "GNU Emacs") " " (a (@ (href "https://orgmode.org/")) "Org Mode") " " (a (@ (href "https://picocss.com/")) "picocss") ) ))
Looks neat for me. At least I don't need to mess with string concatenation. Whole template wiring looks like that. Not much, but it works and easy to maintain.
(defun my/template (contents info) (concat "<!DOCTYPE html>" (sxml-to-xml `(html (@ (lang "en")) (head (meta (@ (charset "utf-8"))) (meta (@ (author "Alex Mikhailov"))) (meta (@ (name "viewport") (content "width=device-width, initial-scale=1, shrink-to-fit=no"))) (meta (@ (name "color-scheme") (content "light dark"))) (meta (@ (http-equiv "content-language") (content "en-us"))) (meta (@ (name "description") (content "Personal page with a blog about my technical adventures"))) (link (@ (rel "icon") (type "image/x-icon") (href "/resources/favicon.ico"))) (link (@ (rel "stylesheet") (href "/resources/css/pico.sand.min.css"))) (script (@ (defer "true") (src "https://umami.dokutsu.xyz/script.js") (data-website-id "d52d9af1-0c7d-4531-84c6-0b9c2850011f")) ()) (title ,(org-export-data (plist-get info :title) info))) (body (main (@ (class "container")) ,(my/header info) (*RAW-STRING* ,contents) ,(my/footer info) ) )) )) )
Ok, now we need some additional steps to wire these templating function.
;; Derive new backend with our custom tepmplating function ;; We derive it from regular HTML backend (org-export-define-derived-backend 'my-html 'html :translate-alist '((template . my/template) )) ;; Define publish function which uses our freshly derived backend (defun my/publish-to-html (plist filename pub-dir) "Publish an Org file to HTML using the custom backend." (org-publish-org-to 'my-html filename ".html" plist pub-dir))
So everything is almost done. Time to use our custom publishing function in projects list.
(setq org-publish-project-alist (list (list "blog" :recursive t :base-directory my/blog-src-path :publishing-directory my/web-export-path :publishing-function 'my/publish-to-html :html-html5-fancy t :htmlized-source t :with-author nil :with-creator t :with-toc t :section-numbers nil :time-stamp-file nil ) ))
Static files
Yep, you may want to publish some photos with your blog or any other static files.
(setq org-publish-project-alist (list (list "static" :base-directory my/blog-src-path :base-extension "css\\|js\\|png\\|jpg\\|jpeg\\|gif\\|pdf\\|ico\\|txt" :publishing-directory my/web-export-path :recursive t :publishing-function 'org-publish-attachment ) ))
Looks self explanatory.
Whole build script
Here is the whole elisp script which I use to publish my blog. It have some additional quirks to work with doomscript ./build-site.el
.
;; Load the publishing system ;; Configure environment ;; (setq debug-on-error t) (let ((default-directory (concat "~/.config/emacs/.local/straight/build-" emacs-version "/"))) (normal-top-level-add-subdirs-to-load-path)) (add-to-list 'custom-theme-load-path (concat "~/.config/emacs/.local/straight/build-" emacs-version "/doom-themes")) (add-to-list 'custom-theme-load-path (concat "~/.config/emacs/.local/straight/build-" emacs-version "/base16-theme")) (add-to-list 'custom-theme-load-path (concat "~/.config/emacs/.local/straight/build-" emacs-version "/moe-theme")) (require 'xml) (require 'dom) (require 'ox-publish) (require 'ox-rss) (require 'org) (require 'esxml) ;; ;;Variables ;; (setq my/url "https://fidonode.me" my/web-export-path "./public" my/blog-src-path "./home/05 Blog" org-html-validation-link nil ;; Don't show validation link org-html-htmlize-output-type 'inline-css org-src-fontify-natively t) ;; ;;Templates ;; (defun my/footer (info) `(footer (@ (class "footer")) (hr) (p "Alex Mikhailov") (p "Built with: " (a (@ (href "https://www.gnu.org/software/emacs/")) "GNU Emacs") " " (a (@ (href "https://orgmode.org/")) "Org Mode") " " (a (@ (href "https://picocss.com/")) "picocss") ) )) (defun my/header (info) `(header (@ (class "header")) (nav (ul (li (strong ,(org-export-data (plist-get info :title) info)))) (ul (li (a (@ (href "/index.html")) "About")) (li (a (@ (href "/blog.html")) "Blog")) (li (a (@ (href "/rss.xml")) "RSS")) ) )) ) (defun my/template (contents info) (concat "<!DOCTYPE html>" (sxml-to-xml `(html (@ (lang "en")) (head (meta (@ (charset "utf-8"))) (meta (@ (author "Alex Mikhailov"))) (meta (@ (name "viewport") (content "width=device-width, initial-scale=1, shrink-to-fit=no"))) (meta (@ (name "color-scheme") (content "light dark"))) (meta (@ (http-equiv "content-language") (content "en-us"))) (meta (@ (name "description") (content "Personal page with a blog about my technical adventures"))) (link (@ (rel "icon") (type "image/x-icon") (href "/resources/favicon.ico"))) (link (@ (rel "stylesheet") (href "/resources/css/pico.sand.min.css"))) (script (@ (defer "true") (src "https://umami.dokutsu.xyz/script.js") (data-website-id "d52d9af1-0c7d-4531-84c6-0b9c2850011f")) ()) (title ,(org-export-data (plist-get info :title) info))) (body (main (@ (class "container")) ,(my/header info) (*RAW-STRING* ,contents) ,(my/footer info) ) )) )) ) (org-export-define-derived-backend 'my-html 'html :translate-alist '((template . my/template) )) (defun my/publish-to-html (plist filename pub-dir) "Publish an Org file to HTML using the custom backend." (org-publish-org-to 'my-html filename ".html" plist pub-dir)) ;; ;;Helpers ;; (defun my/format-date-subtitle (file project) "Format the date found in FILE of PROJECT." (format-time-string "posted on %Y-%m-%d" (org-publish-find-date file project))) ;; ;;Clear folder with results ;; (when (file-directory-p my/web-export-path) (delete-directory my/web-export-path t)) (mkdir my/web-export-path) ;; ;;Main blog configuration ;; (setq org-publish-project-alist (list (list "static" :base-directory my/blog-src-path :base-extension "css\\|js\\|png\\|jpg\\|jpeg\\|gif\\|pdf\\|ico\\|txt" :publishing-directory my/web-export-path :recursive t :publishing-function 'org-publish-attachment ) (list "blog" :recursive t :base-directory my/blog-src-path :publishing-directory my/web-export-path :publishing-function 'my/publish-to-html :html-html5-fancy t :htmlized-source t :with-author nil :with-creator t :with-toc t :section-numbers nil :time-stamp-file nil ) )) ;; Generate the site output (org-publish-all t) (message "Build complete!")
Publish through GitHub Action
With all previous preparations, this step sounds simple like: emacs -Q --script ./build-site.el
I've chosen a pretty standard way to publish static sites through GitHub Pages. Since I keep my Org files in a private repo, I need some additional steps to address it. I use the peaceiris/actions-gh-pages@v3
action to publish from my Org repo to the Pages repo.
However, since I use Doom Emacs
as my configuration framework, we need to address some more problems.
Install Emacs
If you want to run Emacs Lisp
, you need the whole Emacs, at least without GUI. In a GitHub Action, you can simply run:
sudo apt install emacs-nox --yes
This way has a downside - you will install Emacs on each action run since the system state is disposable.
Just bring everything
I need to take extra steps since I use Doom Emacs and have my configs in Org. You may also need to install dependencies for your configuration.
Fetch doom guts
git clone --depth 1 https://github.com/doomemacs/doomemacs ~/.config/emacs
Prepare minimal config for rendering Org file to config.
echo '(doom! :config literate)' > ~/.config/doom/init.el echo '(setq +literate-config-file "'$(pwd)'/config/config.org")' > ~/.config/doom/cli.el
Finally, install all dependencies according to my config. Yes, it is overhead, but I can be sure that I have the same dependencies as on my dev machine.
~/.config/emacs/bin/doom sync -B
Of course, I use a caching step to make the whole process faster:
- name: Cache doom-emacs uses: actions/cache@v4 id: cache-doom-save with: path: ~/.config/emacs key: ${{ runner.os }}-doom
BTW I use GNU Emacs
Here's the whole publishing workflow.
name: pages on: push: branches: - "main" # Do not trigger build on changes in other folders paths-ignore: - "./home/02 Action" - "./home/03 PKM" - "./home/04 Log" - "./home/06 Projects" workflow_dispatch: jobs: build: runs-on: ubuntu-latest steps: - name: Check out uses: actions/checkout@v1 #Install emacs without GUI components - name: Install Emacs run: sudo apt install emacs-nox --yes #Clone doomemacs. Yep, always the most fresh master. Let it fire. - name: Install doom run: git clone --depth 1 https://github.com/doomemacs/doomemacs ~/.config/emacs # Use cached files to shave some time - name: Restore cached doom-emacs id: cache-doom-restore uses: actions/cache/restore@v4 with: path: ~/.config/emacs key: ${{ runner.os }}-doom - name: Create folder run: mkdir -p ~/.config/doom/ # I use literate config, so we need some extra steps to botstrap my config - name: Put template for literate config run: echo '(doom! :config literate)' > ~/.config/doom/init.el # Yep. I also keep my emacs config in org in my org repo - name: Propagate org conf run: echo '(setq +literate-config-file "'$(pwd)'/config/config.org")' > ~/.config/doom/cli.el # Build doomemacs deps. Should be relativelly fast, cause almost everything cached. - name: Sync doom run: ~/.config/emacs/bin/doom sync -B #Put files into cache - name: Cache doom-emacs uses: actions/cache@v4 id: cache-doom-save with: path: ~/.config/emacs key: ${{ runner.os }}-doom - name: Build the site run: ~/.config/emacs/bin/doomscript ./build-site.el # Deploy from this repo to that ~external_repository~ - name: Deploy uses: peaceiris/actions-gh-pages@v3 with: deploy_key: ${{ secrets.PRIVATE_KEY }} external_repository: fido-node/fido-node.github.io publish_branch: gh-pages publish_dir: ./public
What is next
I have a plans to make posts about next features:
RSS
Already implemented. Need to document process.
Sitemap
Not implemented yet.
Code highlighting
Only rudimentary highlight. Want something fancier.