Featured image of post My Emacs Org Mode Blog with Hugo

My Emacs Org Mode Blog with Hugo

Setup a blog in GNU Emacs Org Mode with Hugo

This post is a comprehensive guide to creating and maintaining a Hugo blog using Org mode in Doom Emacs. We’ll cover:

  • Installing Hugo and a theme
  • Configuring ox-hugo
  • Structuring content with Org mode
  • Troubleshooting common issues
  • Deploying your site

If you’re using Emacs, Hugo, and Git, this guide will help you **get up and running smoothly**—without running into the same issues I did!

Pre-requisites

To follow along, ensure you have:

  • Doom Emacs installed (or plain Emacs with Org mode).
  • The +hugo flag enabled in Doom’s init.el:
      (org +roam2 +hugo)  ; Enable Hugo support
    
  • ox-hugo installed (for Org to Hugo exports).
  • A Git repository initialized in your blog directory to enable Hugo’s GitInfo feature.

Caveat: If you want Git-based last modified dates to work, ensure your blog directory is inside a Git repo.

Installing Hugo and Setting Up the Blog

Install Hugo

📌 Important: You must install Hugo extended version (v0.123.0+), or you’ll run into errors related to .Site.Lastmod and SCSS processing.

On Ubuntu, download the DEB manually instead of using apt:

wget https://github.com/gohugoio/hugo/releases/download/v0.144.1/hugo_extended_0.144.1_linux-amd64.deb
sudo dpkg -i hugo_extended_0.144.1_linux-amd64.deb
hugo version  # Verify that it prints v0.144.1 (extended)

If you want to deploy via hugo deploy you will need to download this version instead.

wget https://github.com/gohugoio/hugo/releases/download/v0.144.2/hugo_extended_withdeploy_0.144.2_linux-amd64.deb
sudo dpkg -i hugo_extended_withdeploy_0.144.2_linux-amd64.deb
hugo version  # Verify that it prints v0.144.2 (extended)

Caveat: If you install Hugo via apt, you may get an outdated version that lacks necessary features.

Initialise Hugo

I store my blog inside my “second brain” at ~/brain/blog. Run:

mkdir -p ~/brain/blog
cd ~/brain/blog

Once in here we can create a new site. The new site will have the correct structure but no content or theme yet.

hugo new site .

Add a Theme

I full list of themes can be found here here.

I use the hugo-theme-stack theme, installed as a Git submodule for customization:

git submodule add https://github.com/CaiJimmy/hugo-theme-stack.git themes/hugo-theme-stack

📌 Alternative: You can install it as a Hugo module, which simplifies version management, but I prefer submodules for direct theme customization.

Configuring Hugo

Edit your config.toml file to set up the site:

baseURL = "http://teleoplexy.com/"
languageCode = "en-uk"
title = "Teleoplexy"
theme = "hugo-theme-stack"
enableGitInfo = true

Then we want to do some theme specific configurations. We’ll start with the [params] configuration:

[params]
  mainSections = ["posts"]  # IMPORTANT: Must match your content folder name
  featuredImageField = "image"
  rssFullContent = true
  favicon = "/favicon.ico"
  description = "Your page description here."

  [params.sidebar]
    compact = false
    subtitle = "This description appears in the left sidebar."
    [params.sidebar.avatar]
      enabled = true
      local = true
      src = "img/me.jpg"

  [params.article]
    toc = true
    readingTime = true

Note: This configuration will differ if you have chosen a different theme and it is best to consult their docs.

This is how we configure the navigation in the side bar.

# Menu
[menu]
[[menu.main]]
    name = "Home"
    url = "/"
    weight = -90
    identifier = "home"
    [menu.main.params]
        icon = "home"

  [[menu.main]]
    name = "Posts"
    url = "/posts/"
    weight = -80
    identifier = "posts"
    [menu.main.params]
        icon = "archives"

  [[menu.main]]
    name = "About"
    url = "/about/"
    weight = -70
    identifier = "about"
    [menu.main.params]
        icon = "info-circle"

  [[menu.main]]
    name = "Tags"
    url = "/tags/"
    weight = -60
    identifier = "tags"
    [menu.main.params]
        icon = "tag"

  [[menu.main]]
    name = "Categories"
    url = "/categories/"
    weight = -50
    identifier = "categories"
    [menu.main.params]
        icon = "categories"

To add the social links we add this:

# Social Links
[[menu.social]]
    name = "GitHub"
    url = "https://github.com/jackmarsh"
    weight = 1
    identifier = "github"
    [menu.social.params]
        icon = "brand-github"

[[menu.social]]
    name = "X"
    url = "https://x.com/0xJackMarsh"
    weight = 2
    identifier = "X"
    [menu.social.params]
        icon = "brand-x"

📌 Fix for Missing Icons: If Hugo logs “icon not found” errors, add missing icons to themes/hugo-theme-stack/assets/icons/. You can find similar looking icons for this theme from tabler.


Writing a Post in Org Mode

All Posts

I create a file under ~/brain/blog/org called all_posts.org. This org file will contain all the properties required by ox=hugo for each of the blog posts.

#+HUGO_BASE_DIR: ../

The #+HUGO_BASE_DIR: and :EXPORT_FILE_NAME: property determine where the markdown files are placed. Typically this is in content/posts. See below.

Adding first post

I like to keep each post in it’s own subdirectory so that I can keep pictures and content relevant to this specific post in the same sub dir.

Inside ~/brain/blog/org/first_post/index.org you can write all the content for your first post.

* My first header
Some content in my first post.

Then we pull that into all_posts.org like so.

#+HUGO_BASE_DIR: ../

* DONE My First Post :tag1:@category1:
:PROPERTIES:
:EXPORT_FILE_NAME: my-first-post
:EXPORT_HUGO_TYPE: post
:EXPORT_HUGO_CUSTOM_FRONT_MATTER: :description "An introduction to my first post."
:EXPORT_HUGO_CUSTOM_FRONT_MATTER+: :image "/images/my-first-post.png"
:EXPORT_HUGO_CUSTOM_FRONT_MATTER+: :comments true
:EXPORT_HUGO_CUSTOM_FRONT_MATTER+: :license "CC BY-NC-SA 4.0"
:EXPORT_HUGO_CUSTOM_FRONT_MATTER+: :math false
:EXPORT_HUGO_CUSTOM_FRONT_MATTER+: :toc true
:EXPORT_HUGO_CUSTOM_FRONT_MATTER+: :keywords ["blog" "hugo" "ox-hugo"]
:END:

#+INCLUDE "first_post/index.org"

These properties set the hugo front matter, some of which is needed by the theme I’ve chosen.

Export

With your cursor anywhere on this subheading, export with:

C-c C-e H H or,

M-x then org-hugo-export-wim-to-md


Development Server

To view these changes and make sure everything is working as expected navigate to your terminal and run:

$ hugo serve
Watching for changes in ~/brain/blog/{archetypes,content,data,layouts,static,themes}
Watching for config changes in ~/brain/blog/config.toml, ~/brain/blog/themes/hugo-theme-stack/config.yaml
Start building sites …
hugo v0.144.1-a79d63a44659b6bc76dcdf223de1637e0bd70ff6+extended linux/amd64 BuildDate=2025-02-18T12:14:07Z VendorInfo=gohugoio


                   | EN
-------------------+-----
  Pages            | 30
  Paginator pages  |  0
  Non-page files   |  0
  Static files     |  2
  Processed images |  1
  Aliases          | 10
  Cleaned          |  0

Built in 84 ms
Environment: "development"
Serving pages from disk
Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender
Web Server is available at http://localhost:1313/ (bind address 127.0.0.1)
Press Ctrl+C to stop

You can then open the Web Server which is available on http://localhost:1313.


Publishing The Site

In this step I publish the site, but do not deploy it.

When publishing a site, Hugo create the entire static site in the public directory in the root of the project. This includes HTML files, and assets such as images, CSS files and JavaScript files.

To publish the site, simply run:

hugo

Deploying The Site

I’ve deployed the site with Google Cloud Storeage bucket behind a load balancer.

I just followed the steps here.

After setting up the bucket and load balancer and adding this to the config.toml:

[deployment]
[[deployment.targets]]
name = "production"
URL = "gs://<YOUR-BUCKET-NAME>"

You can then just run:

hugo deploy [--target=<target name>]

Adding new posts

From here on out to add new posts we simply:

  1. Write the post under ~/brain/blog/org/<new_post>/index.org
  2. Add the post, with its properties, to ~/brain/blog/org/all_posts.org
    • When finished, set it to DONE with C-c C-t d
  3. C-c C-e H H or M-x then org-hugo-export-wim-to-md
  4. Then:
       cd ~/brain/blog
       hugo
       hugo deploy
    

Conclusion

We covered:

  • ✅ Setting up Hugo with Org mode
  • ✅ Avoiding common pitfalls
  • ✅ Fixing missing archives & JSON issues
  • ✅ Deploying the site

🚀 Now you have a fully working, Hugo-powered Emacs blog!