Blogging with org-mode

I do almost all my note-taking in Emacs org-mode, so naturally I also prefer to write my blog posts in it. As for my ox-hugo blogging flow, I use the less preferred method: one org file per post, the consequence is I cannot just copy-paste the org capture setup provided by the doc site. I also setup my posts in a subdirectory beneath the HUGO_BASE_DIR:

1
tree -n  ..
Code Snippet 1: my content directory
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
..
├── about
│   └── about.org
├── archives.org
├── emacs-avy
│   ├── avy.gif
│   └── emacs-avy.org
├── emacs-builtin-use-package
│   ├── emacs-builtin-use-package.org
│   ├── prefix-key.png
│   └── which-key-mode.png
├── emacs-evil
│   └── emacs-evil.org
├── emacs-general
│   └── emacs-general.org
├── emacs-sane-defaults
│   ├── default-emacs.png
│   ├── emacs-after.png
│   ├── emacs-dired.png
│   ├── emacs-eshell.png
│   ├── emacs-sane-defaults.org
│   └── emacs-tetris.png
├── emacs-shell
│   ├── emacs-shell.org
│   ├── eshell-command-form.png
│   ├── eshell-elisp-form.png
│   └── eshell.png
├── emacs-version-control
│   ├── emacs-magit-status.png
│   └── emacs-version-control.org
├── emacs-vertico
│   ├── consult-buffer.png
│   ├── consult-flymake.png
│   ├── consult-outline.png
│   ├── consult-yank-pop.png
│   ├── corfu-completion.png
│   ├── emacs-vertico.org
│   ├── emacs-vertico.png
│   ├── emacs-without-vertico.png
│   ├── marginalia-describe-variable.png
│   ├── marginalia-files.png
│   ├── marginalia-find-file-with-icons.png
│   ├── nerd-icons-corfu.png
│   ├── orderless-default.png
│   ├── vertico-buffer-mode.png
│   ├── vertico-flat-mode.png
│   ├── vertico-grid-mode.png
│   ├── vertico-indexed-mode.png
│   ├── vertico-quick.png
│   └── vertico-reverse-mode.png
├── github-action-hugo-emacs
│   ├── action-secrets-and-variables.png
│   ├── deploy-github.png
│   └── github-action-hugo-emacs.org
├── hugo-blogging-org-capture-templates
│   └── hugo-blogging-org-capture-templates.org
├── hyprland
│   ├── hyprland.org
│   └── hyprland.png
├── notmuch-mail-emacs
│   ├── full.png
│   └── notmuch-mail-emacs.org
├── search.org
└── theming-emacs
    ├── default-emacs.png
    ├── emacs-doom-theme-modeline-and-dashboard.png
    ├── emacs-wombat.png
    └── theming-emacs.org

15 directories, 53 files

Custom org-capture-templates

First off, org-capture-templates is just like its name; it is a template for creation of new entries. It is used by org-mode, which is a killer feature of Emacs. Initially it was design to capture notes with little interruption1. But since it was all Emacs Lisp, we can modify it with ease.

basic template

1
2
3
4
5
(setq org-capture-templates
      '(("t" "Todo" entry (file+headline "~/org/gtd.org" "Tasks")
         "* TODO %?\n  %i\n  %a")
        ("j" "Journal" entry (file+datetree "~/org/journal.org")
         "* %?\nEntered on %U\n  %i\n  %a")))
Code Snippet 2: from the manual

org-capture-templates is a list of:

keys
in the example, t is for todo entry and j is for journal.
description
usually a one-liner describing what kind of capture the key is.
type
the type of the entry, here entry is an org node with a headline.
target
where the capture should be placed.
template
the template itself2.

So I need to tweak it a bit in order to automatically create a file within a subdirectory in my blog content (using the same name to make it easier). Not only that, ox-hugo use an org meta-data for hugo front-matter3. Each new files created must be started with these metadata (at minimal):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#+options: ':nil -:nil ^:{} num:nil toc:nil
#+author: Kristian Alexander P
#+creator: Emacs 29.2 (Org mode 9.6.15 + ox-hugo)
#+hugo_section: posts
#+hugo_base_dir: ../../
#+date: <2024-03-03 Sun>
#+title: Hugo blog org-capture-templates
#+description: My blogging workflow
#+hugo_tags: hugo emacs org
#+hugo_categories: emacs
#+hugo_auto_set_lastmod: t
#+startup: inlineimages
Code Snippet 3: ox-hugo metadata

Some metadata will be different for each capture; title, description, hugo_tags, and hugo_categories. dates should be set as the capture date, the other will be needing a user input, including the filename, for the org-capture process.

Those will be set as the template part. As for the target, I’m using a simple Emacs Lisp function:

1
2
3
4
5
(defun +config/create-new-blog-post ()
  "Create new blog post path."
  (interactive)
  (let ((name (read-string "Filename: ")))
    (concat +config/blog-directory "/content-org/" (format "%s" name) "/" (format "%s.org" name))))
Code Snippet 4: my targeting function

This function is just a basic input/output; it will ask for a file name, and then it will concatenate it as a valid file path, here subtituted as a variable +config/blog-directory

1
2
3
(when (file-directory-p (expand-file-name "alexforsale.github.io" org-directory))
  (customize-set-variable '+config/blog-directory
                          (expand-file-name "alexforsale.github.io" org-directory)))
Code Snippet 5: the variable definition

Basically it will look for a directory named “alexforsale.github.io” (it’s my github-page repository) inside the variable org-directory, which is also should be set.

The actual template

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
(add-to-list 'org-capture-templates
             '("b" "(b)log post" plain
               (file +config/create-new-blog-post)
               "\
#+options: ':nil -:nil ^:{} num:nil toc:nil
#+author: %n
#+creator: Emacs %(eval emacs-version) (Org mode %(eval org-version) + ox-hugo)
#+hugo_section: posts
#+hugo_base_dir: ../../
#+date: %t
#+title: %^{title}
#+hugo_draft: true
#+description: %^{description}
#+hugo_tags: %^{tags}
#+hugo_categories: %^{categories}
#+hugo_auto_set_lastmod: t
#+startup: inlineimages\n%?" :unnarrowed t :jump-to-captured t))
Code Snippet 6: my capture template

This will create a draft post, to publish it set the draft to false. Some template expansions I use:

%n
this will expand to the variable user-full-name.
%t
date.
%^{title}, %{description}, %^{tags}, and %^{categories}
will prompt for the user for each metadata.
\n
is for newline.
%?
will be the point location.

Usually, org-capture is not bound to any keys, the recommended way is to bind it to C-c c4. So to use this template the keybinding is C-c c b.

1
2
3
(global-set-key (kbd "C-c l") #'org-store-link)
(global-set-key (kbd "C-c a") #'org-agenda)
(global-set-key (kbd "C-c c") #'org-capture)
Code Snippet 7: the recommended keybindings

Preview hugo blog locally

Before pushing each commits to my github repository, I’d view my blog in my local machine, With hugo this can be done by running:

1
hugo server --buildDrafts --navigateToChanged
Code Snippet 8: hugo server

from within the HUGO_BASE_DIR directory. This can be done from a terminal emulator, or, since I’m using Emacs, I can run it using async-shell-command, which is actually shell-command, but adds a & at the end of the command to run it asynchronously.

1
(async-shell-command "hugo server --buildDrafts --navigateToChanged &" "*hugo*" "*hugo-error*")
Code Snippet 9: running hugo server within emacs

The *hugo* argument is the output-buffer, and *hugo-error* is the error-buffer. But since the hugo server command has many other flags, I use an external package called transient5 to toggle each flags.

1
hugo server --help
Code Snippet 10: hugo server help
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
Hugo provides its own webserver which builds and serves the site.
While hugo server is high performance, it is a webserver with limited options.

'hugo server' will by default write and server files from disk, but you can
render to memory by using the '--renderToMemory' flag. This can be faster
in some cases, but it will consume more memory.

By default hugo will also watch your files for any changes you make and
automatically rebuild the site. It will then live reload any open browser pages
and push the latest content to them. As most Hugo sites are built in a fraction
of a second, you will be able to save and see your changes nearly instantly.

Usage:
  hugo server [command] [flags]
  hugo server [command]

Aliases:
  server, serve

Available Commands:
  trust       Install the local CA in the system trust store.

Flags:
      --appendPort             append port to baseURL (default true)
  -b, --baseURL string         hostname (and path) to the root, e.g. https://spf13.com/
      --bind string            interface to which the server will bind (default "127.0.0.1")
  -D, --buildDrafts            include content marked as draft
  -E, --buildExpired           include expired content
  -F, --buildFuture            include content with publishdate in the future
      --cacheDir string        filesystem path to cache directory
      --cleanDestinationDir    remove files from destination not found in static directories
  -c, --contentDir string      filesystem path to content directory
      --disableBrowserError    do not show build errors in the browser
      --disableFastRender      enables full re-renders on changes
      --disableKinds strings   disable different kind of pages (home, RSS etc.)
      --disableLiveReload      watch without enabling live browser reload on rebuild
      --enableGitInfo          add Git revision, date, author, and CODEOWNERS info to the pages
      --forceSyncStatic        copy all files when static is changed.
      --gc                     enable to run some cleanup tasks (remove unused cache files) after the build
  -h, --help                   help for server
      --ignoreCache            ignores the cache directory
  -l, --layoutDir string       filesystem path to layout directory
      --liveReloadPort int     port for live reloading (i.e. 443 in HTTPS proxy situations) (default -1)
      --minify                 minify any supported output format (HTML, XML etc.)
      --navigateToChanged      navigate to changed content file on live browser reload
      --noBuildLock            don't create .hugo_build.lock file
      --noChmod                don't sync permission mode of files
      --noHTTPCache            prevent HTTP caching
      --noTimes                don't sync modification time of files
      --panicOnWarning         panic on first WARNING log
      --poll string            set this to a poll interval, e.g --poll 700ms, to use a poll based approach to watch for file system changes
  -p, --port int               port on which the server will listen (default 1313)
      --pprof                  enable the pprof server (port 8080)
      --printI18nWarnings      print missing translations
      --printMemoryUsage       print memory usage to screen at intervals
      --printPathWarnings      print warnings on duplicate target paths etc.
      --printUnusedTemplates   print warnings on unused templates.
      --renderStaticToDisk     serve static files from disk and dynamic files from memory
      --templateMetrics        display metrics about template executions
      --templateMetricsHints   calculate some improvement hints when combined with --templateMetrics
  -t, --theme strings          themes to use (located in /themes/THEMENAME/)
      --tlsAuto                generate and use locally-trusted certificates.
      --tlsCertFile string     path to TLS certificate file
      --tlsKeyFile string      path to TLS key file
      --trace file             write trace to file (not useful in general)
  -w, --watch                  watch filesystem for changes and recreate as needed (default true)

Global Flags:
      --clock string               set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00
      --config string              config file (default is hugo.yaml|json|toml)
      --configDir string           config dir (default "config")
      --debug                      debug output
  -d, --destination string         filesystem path to write files to
  -e, --environment string         build environment
      --ignoreVendorPaths string   ignores any _vendor for module paths matching the given Glob pattern
      --logLevel string            log level (debug|info|warn|error)
      --quiet                      build in quiet mode
      --renderToMemory             render to memory (mostly useful when running the server)
  -s, --source string              filesystem path to read files relative from
      --themesDir string           filesystem path to themes directory
  -v, --verbose                    verbose output

Use "hugo server [command] --help" for more information about a command.

Then I just use transient-define-prefix to create the command.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
(use-package transient
  :config
  (transient-define-prefix +config/transient-hugo-server ()
    "Run hugo server with `transient'."
    :man-page "hugo-server"
    ["Options"
     ("q" "quit" transient-quit-all)
     ("-D" "Build drafts" "--buildDrafts")
     ("-E" "Build expired" "--buildExpired")
     ("-F" "Build future" "--buildFuture")
     ("-d" "Debug" "--debug")
     ("-B" "Disable build errors on browser" "--disableBrowserError")
     ("-c" "Clean destination dir" "--cleanDestinationDir")
     ("-e" "Enable Git info" "--enableGitInfo")
     ("-F" "enable full re-renders on changes" "--disableFastRender")
     ("-f" "Force sync static files" "--forceSyncStatic")
     ("-g" "enable to run some cleanup tasks" "--gc")
     ("-m" "Minify any supported output format" "--minify")
     ("-C" "No chmod" "--noChmod")
     ("-T" "Don't sync modification time of files" "--noTimes")
     ("-I" "Print missing translation" "--printI18nWarnings")
     ("-M" "Print memory usage" "--printMemoryUsage")
     ("-P" "Print warning on duplicate target path" "--printPathWarnings")
     ("-q" "Quiet" "--quiet")
     ("-v" "Verbose" "--verbose")
     ("-w" "Watch filesystem for changes" "--watch")]
    ["Action"
     ("s" "hugo server" +config/start-hugo-server)]))
Code Snippet 11: install transient with use-package and define our hugo server command, see their showcase to learn how to use transient. I use this heavily in the past.

Note that the “Action” (s) is +config/start-hugo-server which we need to define:

1
2
3
4
5
6
7
8
(defun +config/start-hugo-server (args)
  "Start hugo server in `+config/blog-directory'."
  (interactive (list (transient-args '+config/transient-hugo-server)))
  (if (not (executable-find "hugo"))
      (message "hugo executable not found")
    (let ((default-directory +config/blog-directory)
          (command "hugo server"))
      (async-shell-command (mapconcat #'identity `(,command ,@args) " ") "*hugo*" "*hugo-error*"))))
Code Snippet 12: the function

This function will run hugo server, with additional args which will be provided by the transient command.

  • the mapconcat will apply the first argument to each element of the second arguments, for example:

    1
    
    (mapconcat #'identity '("abc" "def" "ghi") ". ")
    
    Code Snippet 13: mapconcat example
    1
    
    "abc. def. ghi"
    
  • the identity simply returns the arguments unchanged.

    Figure 1: transient in action

    Figure 1: transient in action