Build a static site with Hugo and Orgmode.
Static site generators are getting popular this years. I was thinking a time ago about creating one with gatsby or jekyll. But I discover the power of org-mode and how to organize my life with simple text files. And the next step is to write a blog with my editor only.
Requirements.
- Emacs
- Hugo
- ox-hugo
- git
Create new Hugo site
First we need to create the basic Hugo directory structure.
Create a new project site with this command:
hugo new site kcurtet.github.io # remplace with your site folder
Then we can see the directory structure like this:
kcurtet.github.io
├── archetypes
├── config.toml
├── content
├── data
├── layouts
├── public
├── resources
├── static
└── themes
The basic configuration for the site its done in config.toml
Initialize a new git repository.
cd kcurtet.github.io
git init
git add .
git commit -m "Initial commit"
Now we need to add a default theme four our blog.
In my case I choose the Cactus Theme
git submodule add https://github.com/monkeyWzr/hugo-theme-cactus themes/cactus
Now add in the file config.toml
the theme we have downloaded.
theme = "cactus"
This configures Hugo to utilize cactus
as base theme.
Install ox-hugo to create the posts.
We need ox-hugo
to export org-mode
files to Hugo’s Backfriday Markdown
Vanilla Emacs
This configuration its for pure emacs.
;; Maybe you need to add melpa repository in your `init.el' file
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)
;; M-x install-package RET ox-hugo
;; Then load it after `ox'
(eval-after-load 'ox
(require 'ox-hugo))
Doom
In doom.d/init.el remove the comment for org
module and add the +hugo
flag
(org
+hugo)
use-package
More info: https://github.com/jwiegley/use-package
(use-package ox-hugo
:after 'ox)
Write the content
Now its time to write some blog posts. First we create a org file in the content folder of our project.
touch content/posts.org
In the beginning of the file we add this headers to loacate our project.
#+HUGO_BASE_DIR: ../ # Hugo base project folder
#+HUGO_SECTION: posts # Hugo folder in content
HUGO_BASE_DIR
: The project root. In this case is the parent folder of the file.
HUGO_SECTION
creates a folder content/posts
where we save the markdown files.
Then you need to create your first blog post.
Start with a Heading or Subheading to add some properties to it.
* TODO Hello this is my first blog post
:PROPERTIES:
:EXPORT_FILE_NAME: my-first-post-with-hugo-and-org-mode
:EXPORT_DATE: 2021-03-20
:EXPORT_HUGO_MENU: :menu "main"
:END:
Text to introduce my post.
** Sub titles of the post
More text...
In this example we can create as many headers as posts we want, then change the properties to export the content to hugo with the correct file path and date.
EXPORT_FILE_NAME
the file name of the post in content/posts
also used as slug.
EXPORT_DATE
The date of the publication.
EXPORT_HUGO_MENU: :menu "main"
Add this entry to the main menu.
The TODO
keyword sets the posts as drafts.
When you are ready to publish your post type C-c C-e H H
to create the markdown files in content/posts
.
.
├── archetypes
├── config.toml
├── content
└── posts
└── my-first-post-with-hugo-and-org-mod
├── data
├── layouts
├── public
├── resources
├── static
└── themes
Build the static site.
We can preview the contents in a local server with:
hugo server -D # -D to see the drafts.
Or we can build the blog with this command:
hugo --minify
Now in we have a new /public
folder with our static site.
CI/CD Build and upload with GitHub pages.
We want to build our blog every time we upload the project to github.
First we create a file .github/workflows/gh-pages.yaml
in the git repository.
mkdir -p .github/workflows
touch .github/workflows/gh-pages.yaml
And we add this content:
name: github pages
on:
push:
branches:
- main # Set a branch to deploy
jobs:
deploy:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
with:
submodules: true # Fetch Hugo themes (true OR recursive)
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
- name: Setup Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: '0.81.0'
# extended: true
- name: Build
run: hugo --minify
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./public
We need a token to push our build to gh-pages branch. Follow this instructions to add secrets for deploy actions.
When we setup all correctly. we can push our repo to GitHub.
And the action should have created a gh-pages branch with the static site.
Maybe you need to edit the settings of the repository in github to set gh-pages correctly if you don’t see your site.
Don’t forget to set your baseUrl
in config.toml
or Hugo can’t link the assets.
Bonus: Script to export the posts
This script is to export my posts with just emacs istalled and internet conexion.
:;exec emacs --batch --quick --load="$0" --funcall=main "$@"
;;; publish.el --- Publish ox-hugo file subtree -*- lexical-binding: t -*-
;; Copyright (c) 2020 Kevin Curtet <kcurtet@gmail.com>
;; Author: Kevin Curtet <kcurtet@gmail.com>
;; Version: 1.0
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Script to export posts from org file with ox-hugo headers.
;; More info on https://kcuret.github.io
;; Usage: publish.el FILE-NAME [ SUBTREE ]
;; if SUBTREE is given it search for a subtree with a regex pattern
;; and only exports that one.
;;; Code:
;; Better errors
(setq debug-on-error t)
(defvar +cache-dir
(concat (file-name-directory load-file-name) ".packages/")
"Cache folder where `straight.el' downloads the packages.")
(defvar +publish--package-list '(org ox-hugo)
"Packages to be installed with `straight.el'.")
(defun +publish--install-dependencies ()
"Install `straight.el' and `+publish--package-list'."
(let ((straight-base-dir +cache-dir)
(bootstrap-file
(expand-file-name "straight/repos/straight.el/bootstrap.el" +cache-dir))
(bootstrap-version 5))
(unless (file-exists-p bootstrap-file)
(with-current-buffer
(url-retrieve-synchronously
"https://raw.githubusercontent.com/raxod502/straight.el/develop/install.el"
'silent 'inhibit-cookies)
(goto-char (point-max))
(eval-print-last-sexp)))
(load bootstrap-file nil 'nomessage)
;; Install & load packages in `publish-package-list'.
(dolist (package +publish--package-list)
(straight-use-package package)
(require package))))
(defun +publish--open-file (file-name)
"Open FILE-NAME as read only."
(find-file-read-only (expand-file-name file-name))
(goto-char (point-min)))
(defun main ()
"Export FILE-NAME ox-hugo posts."
(+publush--install-dependencies)
(pcase command-line-args-left
;; Take FILE-NAME and SUBTREE from args
(`(,file-name ,subtree)
(+publish--open-file file-name)
(if (re-search-forward (concat "^*+ " subtree) nil t)
(message "%s" (org-hugo-export-wim-to-md nil nil t t))
(error "There is no subtree %s in %s" subtree file-name)))
;; Take FILE-NAME from args
(`(,file-name)
(+publish--open-file file-name)
(org-hugo-export-wim-to-md t nil nil t))
(x
(error "Usage: publish.el FILE [ SUBTREE ]"))))
(provide 'publish)
;;; publish.el ends here
The first line invokes emacs in batch mode with a trick :;
(I get this line from a script in guix sources).
With the --load
we load the file contents then we run the main function with --funcall
and we pass the arguments to emacs with "$@"
.
:;exec emacs --batch --quick --load="$0" --funcall=main "$@"
Now we can run this script from the shell like any other script.
./package.el # Usage: package.el FILE-NAME [SUBTREE]