Create a blog with Asciidoctor and Hugo
This blog is a static website generated by Hugo using asciidoctor content. In this article I explain how I did it.
Foreword
Some basic git and web hosting notions are required to understand what follows, I won’t go into the details of those.
The given commands are for Ubuntu/Debian. You will have to adapt them for any non Debian derivated OS.
Why?
Why not? Well let’s just say I did not intend to create a blog, I am just used to writing documentation in asciidoc because I find it more flexible than Markdown and offers more possibilities.
I had already used Hugo to generate a website for my resumé so I was accustomed to it.
After having documented my Yunohost instance backup with restic, I posted it on the Yunohost forums.
I had to edit it multiple times to fix or add some things and got sick of doing it on the forums, I always had to convert asciidoc to markdown.
I know there are tools out there for this but some asciidoc features don’t exist in markdown (to my knowledge). That’s when the website idea came up.
Furthermore on the Yunohost forums I had posted my article in french and english, one more reason to publish a website leveraging Hugo multilingual mode.
To sum things up, this gives me everything I want:
-
I write in a text format I love
-
I write with my favourite editor
-
I can version control my content
-
I use my favourite DevOps platform (sef hosted using Yunohost) for continuous delivery on new content creation (more on this in a future article).
-
I don’t need to manage a database or any hypertext preprocessor on my webserver
How?
The quick start section of the Hugo website is clear enough, the only difference in our case is the .adoc
extension for the created articles
In short:
-
Install some requirements
apt-get update && apt-get install rubygems wget git unzip
-
Install asciidoctor
gem install asciidoctor
-
Install hugo
cd /tmp/ wget https://github.com/gohugoio/hugo/releases/download/v0.64.1/hugo_0.64.1_Linux-64bit.tar.gz -O hugo.tgz tar -xf hugo.tgz mv hugo /usr/local/bin/
-
Create a new site
hugo new site mysite
-
Create an article
hugo new posts/myfirstpost.adoc (1) cat << EOPOST > content/posts/myfirstpost.adoc --- title: "My first post" date: 2020-02-17T21:34:00Z draft: true --- = My First post (2) :toc: This is my first post == First section Some content == Second section Some more content EOPOST
1 We use the adoc
extension to tell Hugo to use asciidoctor to generate the pages2 Be careful not to leave any space between the front matter (The part between the ---
) and the start of the asciidoc document, this would prevent asciidoctor to generate the files. -
Add a theme
git init (1) git submodule add https://github.com/budparr/gohugo-theme-ananke.git themes/ananke echo 'theme = "ananke"' >> config.toml
1 Since we are manipulating text files, we can version control everything -
Launch Hugo development environment
hugo serve -D
We tell Hugo to process drafts of articles with the
-D
argument
To visit the site, go to http://localhost:1313.
Once we are satisfied with the results and we want to go to production:
-
Set the
draft
variable to false -
Generate the files:
hugo -b https://subdomain.domain.tld
We need to set the base URL of the website, Hugo will use it to generate links. If the site runs on a subpath of the domain, we need to set it here:
hugo -b https://subdomain.domain.tld/subpath
-
Transfer the content of the
public
directory to the root of the web server
With a few commands we built a website from content written in asciidoc.
I could have stopped there but I wanted to go a little further…
Going further
There were some missing details to the site produced.
Social media icons removal
The default theme displays social media icons. I don’t want that.
fortunately we can override the theme templates:
-
Create a
layouts/_default
directory at the root of the projectmkdir layouts/_default -p
-
Copy the theme pages template into the created directory
cp themes/ananke/layouts/_default/single.html layouts/_default/
-
Remove the
{{ partial "social-share.html" }}
line from the file
Replicate asciidoctor style for admonitions and callouts
I like the style of documents generated by Asciidoctor and right now my website does not look anything like it. Some icons are missing (admonitions) and callouts are quite dull:
I replicated what René Gielen describes in his article on this topic (and added some little things):
-
Create a
static/css
directory at the root of the project.mkdir static/css -p
-
Install rouge
gem install rouge
-
Generate the rouge
molokai
CSS filerougify style molokai > static/css/molokai.css
-
Create a custom CSS file
static/css/custom.css
/* AsciiDoctor*/ table{border-collapse:collapse;border-spacing:0} .admonitionblock>table{border-collapse:separate;border:0;background:none;width:100%} .admonitionblock>table td.icon{text-align:center;width:80px} .admonitionblock>table td.icon img{max-width:none} .admonitionblock>table td.icon .title{font-weight:bold;font-family:"Open Sans","DejaVu Sans",sans-serif;text-transform:uppercase} .admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #ddddd8;color:rgba(0,0,0,.6)} .admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0} .admonitionblock td.icon [class^="fa icon-"]{font-size:2.5em;text-shadow:1px 1px 2px rgba(0,0,0,.5);cursor:default} .admonitionblock td.icon .icon-note::before{content:"\f05a";color:#19407c} .admonitionblock td.icon .icon-tip::before{content:"\f0eb";text-shadow:1px 1px 2px rgba(155,155,0,.8);color:#111} .admonitionblock td.icon .icon-warning::before{content:"\f071";color:#bf6900} .admonitionblock td.icon .icon-caution::before{content:"\f06d";color:#bf3400} .admonitionblock td.icon .icon-important::before{content:"\f06a";color:#bf0000} .conum[data-value]{display:inline-block;color:#fff!important;background-color:rgba(100,100,0,.8);-webkit-border-radius:100px;border-radius:100px;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-family:"Open Sans","DejaVu Sans",sans-serif;font-style:normal;font-weight:bold} .conum[data-value] *{color:#fff!important} .conum[data-value]+b{display:none} .conum[data-value]::after{content:attr(data-value)} pre .conum[data-value]{position:relative;top:-.125em} b.conum *{color:inherit!important} .conum:not([data-value]):empty{display:none} /* images */ img { padding:1px; border:2px solid #357edd; background-color: #333; } /* code inline `code` */ p code { border-radius: 5px; -moz-border-radius: 5px; -webkit-border-radius: 5px; border: 1px solid #BCBEC0; padding: 2px; font:12px Monaco,Consolas,"Andale Mono","DejaVu Sans Mono",monospace } /* block titles `.Some title` */ .listingblock .title { text-rendering:optimizeLegibility; text-align:left; font-family:"Noto Serif","DejaVu Serif",serif; font-size:0.9em; font-style:italic; line-height:1.5; color:#7a2518; font-weight:400; } /* callouts */ .colist td { font-size: 0.8em; padding: 5px; } /* justified paragraphs */ p { text-align: justify; text-justify: inter-word; } /* code highlight */ .hll { background-color: green; text-shadow: 1px 1px 2px white, 0 0 1em black, 0 0 0.2em blue; font-color: "black"; }
-
tell Hugo to use those files by modifying
config.toml
:[params] # Custom CSS custom_css = ["css/molokai.css","css/custom.css"]
Once this is done, things look a bit better but one essential item is missing, the Awesome fonts which provides the icons for amongst other things our admonitions.
So I followed the documentation:
-
Download the font archive
wget https://use.fontawesome.com/releases/v5.12.1/fontawesome-free-5.12.1-web.zip -O /tmp/fontawesome.zip unzip /tmp/fontawesome.zip -d /tmp/
-
Copy the css file and the required fonts in our project
cp -r /tmp/fontawesome-free-*/webfonts static/ cp /tmp/fontawesome-free-*/css/all.css static/css/font-awesome.css
-
Tell Hugo to use those new fonts by updating
config.toml
[params] # Custom CSS custom_css = ["css/molokai.css","css/custom.css","css/font-awesome.css"]
Here we go, I finally got what I wanted.
Featured images and authors names
The Ananke theme allows us to illustrate each article with an image. This image will be displayed with the article title and description on the site frontpage and as a header on the article page itself.
The front page can also have a header image.
As described in the documentation:
-
Create a directory for the images
mkdir -p static/images
-
Copy the images inside it
-
Declare the front page featured image by updating
config.toml
[params] # Custom CSS custom_css = ["css/molokai.css","css/custom.css","css/font-awesome.css"] featured_image = '/images/nom-du-fichier-image.extension'
-
Declare each article featured image by adding this line to the front matter
featured_image: '/images/nom-du-fichier-image.extension'
Now, the website is starting to look nice.
That’s cool but I really don’t know anything about graphic design and I don’t have any photograph matching my articles themes. So I went looking for some royalty-free images on Pixabay. The nice thing to do now is to give credit to the images authors (as you are reminded each time you download something from pixabay)
That’s how I dit it:
-
Declare a
featured_image_by
variable for the front pageconfig.toml[params] # Custom CSS custom_css = ["css/molokai.css","css/custom.css","css/font-awesome.css"] featured_image = '/images/nom-du-fichier-image.extension' featured_image_by = 'Image by <Author> from Pixabay'
-
Same thing for each article front matter
featured_image_by: 'Image by <Author> from Pixabay'
-
Override the theme header definition files
mkdir layouts/partials -p cp themes/ananke/layouts/partials/page-header.html layouts/partials/ cp themes/ananke/layouts/partials/site-header.html layouts/partials/
-
Modify the
layouts/partials/page-header.html
file1 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
{{ $featured_image := .Params.featured_image }} {{ if $featured_image }} {{/* Trimming the slash and adding absURL make sure the image works no matter where our site lives */}} {{ $featured_image := (trim $featured_image "/") | absURL }} <header class="cover bg-top" style="background-image: url('{{ $featured_image }}');"> <div class="pb3-m pb6-l bg-black-60"> {{ partial "site-navigation.html" . }} <div class="tc-l pv6 ph3 ph4-ns"> {{ if not .Params.omit_header_text }} <h1 class="f2 f1-l fw2 white-90 mb0 lh-title">{{ .Title | default .Site.Title }}</h1> {{ with .Params.description }} <h2 class="fw1 f5 f3-l white-80 measure-wide-l center lh-copy mt3 mb4"> {{ . }} </h2> {{ end }} {{ end }} </div> </div> {{ if .Params.featured_image_by }}<div class="bg-black-60" style='text-shadow: 1px 1px 2px white, 0 0 1em black, 0 0 0.2em blue; font-color: "white"; background-color: "black"; position: "bottom"'>{{ .Params.featured_image_by }}</div>{{ end }} (1) </header> {{ else }} <header> <div class="{{ .Site.Params.background_color_class | default "bg-black" }}"> {{ partial "site-navigation.html" . }} </div> </header> {{ end }}
1 If the variable exists, display its content -
Modify the
layouts/partials/page-header.html
file1 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
{{ $featured_image := .Param "featured_image"}} {{ $featured_image_by := .Param "featured_image_by"}} (1) {{ if $featured_image }} {{/* Trimming the slash and adding absURL make sure the image works no matter where our site lives */}} {{ $featured_image := (trim $featured_image "/") | absURL }} <header class="cover bg-top" style="background-image: url('{{ $featured_image }}');"> <div class="{{ .Site.Params.cover_dimming_class | default "bg-black-60" }}"> {{ partial "site-navigation.html" .}} <div class="tc-l pv4 pv6-l ph3 ph4-ns"> <h1 class="f2 f-subheadline-l fw2 white-90 mb0 lh-title"> {{ .Title | default .Site.Title }} </h1> {{ with .Params.description }} <h2 class="fw1 f5 f3-l white-80 measure-wide-l center mt3"> {{ . }} </h2> {{ end }} </div> {{ if $featured_image_by }}<div style='text-shadow: 1px 1px 2px white, 0 0 1em black, 0 0 0.2em blue; font-color: "white"; background-color: "black";'>{{ $featured_image_by }}</div>{{ end }} (2) </div> </header> {{ else }} <header> <div class="pb3-m pb6-l {{ .Site.Params.background_color_class | default "bg-black" }}"> {{ partial "site-navigation.html" . }} <div class="tc-l pv3 ph3 ph4-ns"> <h1 class="f2 f-subheadline-l fw2 light-silver mb0 lh-title"> {{ .Title | default .Site.Title }} </h1> {{ with .Params.description }} <h2 class="fw1 f5 f3-l white-80 measure-wide-l center lh-copy mt3 mb4"> {{ . }} </h2> {{ end }} </div> </div> </header> {{ end }}
1 Store the parameter in a variable for easy access 2 If the variable is not empty, display its content
We now have featured images and gave credit to their author using shadowed text to make sure it is always visible.
I am as bad in CSS as in graphic design, so anyone could do much better but that’s enough for me, at least for now :p |
Make drafts transparent on preview sites
Hugo uses a draft system, usually someone would process drafts only at development stage but not in production.
The thing is, I sometimes forget to unset the article as draft when it’s done and think everything is published when in fact it’s not.
I looked for a simple solution and found this one: lower draft articles opacity to make it clear to my eyes that those are not going to production.
It’s more or less the same thing as previously, override the theme and apply some CSS based on the Draft
variable value:
-
Copy the files to be overriden
cp themes/ananke/layouts/partials/summary-with-image.html layouts/partials/ cp themes/ananke/layouts/_default/baseof.html layouts/_default/
-
Apply the required changes
layouts/partials/summary-with-image.html{{ $featured_image := .Params.featured_image }} <article class="{{ if .Draft }}draft {{ end }}bb b--black-10"> (1) ...
1 Note that we are just applying an additional class to the article when it’s a draft layouts/_default/baseof.html1 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
<!DOCTYPE html> <html lang="{{ $.Site.LanguageCode | default "en" }}"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> {{/* NOTE: the Site's title, and if there is a page title, that is set too */}} <title>{{ block "title" . }}{{ .Site.Title }} {{ with .Params.Title }} | {{ . }}{{ end }}{{ end }}</title> <meta name="viewport" content="width=device-width,minimum-scale=1"> {{ hugo.Generator }} {{/* NOTE: For Production make sure you add `HUGO_ENV="production"` before your build command */}} {{ if eq (getenv "HUGO_ENV") "production" | or (eq .Site.Params.env "production") }} <META NAME="ROBOTS" CONTENT="INDEX, FOLLOW"> {{ else }} <META NAME="ROBOTS" CONTENT="NOINDEX, NOFOLLOW"> {{ end }} {{ $stylesheet := .Site.Data.webpack_assets.app }} {{ with $stylesheet.css }} <link href="{{ relURL (printf "%s%s" "dist/" .) }}" rel="stylesheet"> {{ end }} {{ range .Site.Params.custom_css }} <link rel="stylesheet" href="{{ relURL ($.Site.BaseURL) }}{{ . }}"> {{ end }} {{ block "favicon" . }} {{ partialCached "site-favicon.html" . }} {{ end }} {{ if .OutputFormats.Get "RSS" }} {{ with .OutputFormats.Get "RSS" }} <link href="{{ .RelPermalink }}" rel="alternate" type="application/rss+xml" title="{{ $.Site.Title }}" /> <link href="{{ .RelPermalink }}" rel="feed" type="application/rss+xml" title="{{ $.Site.Title }}" /> {{ end }} {{ end }} {{/* NOTE: These Hugo Internal Templates can be found starting at https://github.com/spf13/hugo/blob/master/tpl/tplimpl/template_embedded.go#L158 */}} {{- template "_internal/opengraph.html" . -}} {{- template "_internal/schema.html" . -}} {{- template "_internal/twitter_cards.html" . -}} {{ if eq (getenv "HUGO_ENV") "production" | or (eq .Site.Params.env "production") }} {{ template "_internal/google_analytics_async.html" . }} {{ end }} </head> <body class="{{ if .Draft }}draft {{ end }}ma0 {{ $.Param "body_classes" | default "avenir bg-near-white"}}{{ with getenv "HUGO_ENV" }} {{ . }}{{ end }}"> (1) {{ block "header" . }}{{ partial "site-header.html" .}}{{ end }} <main class="pb7" role="main"> {{ block "main" . }}{{ end }} </main> {{ block "footer" . }}{{ partialCached "site-footer.html" . }}{{ end }} {{ block "scripts" . }}{{ partialCached "site-scripts.html" . }}{{ end }} </body> </html>
1 same here -
Finally declare the
draft
class by appending tostatic/css/custom.css
:/* drafts */ .draft { opacity:0.5; }
Internationalization
Last but not least, since I had posted in french and english on the Yunohost forums, I though I would be nice to have a multilingual site as well.
I just followed the Hugo documentation.
I chose to separate english and french content in two directories.
The end result is this little en
or fr
label in the upper right corner of the website (provided each article has been translated!)
Going the extra mile
The article is already long enough, I will discuss the following matters in separated posts:
-
Using a docker image and docker-compose to generate the website
-
Publishing a preproduction site
-
Continuously delivering the website with Gitlab-CI
-
Continuously delivering the docker image with Gitlab-CI