nox.im · All Posts · All in Go
Since the Eternal September, the internet has
changed forever. The 1990 were dominated with a wave of creativity of early adopters, the 2000
marked the peak of internet culture and since the 2010s we’re declining towards a sterile network,
dominated by a handful of apps. Entertainment is consumed in form of FAANG content and communication
has deteriorated to 140 280 character exchanges on censored and heavily policed platforms. Blogs
have become rare and are often only found in form of medium posts.
I like the smol internet. Even in the modern web, articles are fundamentally plain text. The Gopher protocol is currently undergoing a renaissance and has spawned some rethinking and the gemini protocol. There is a small but thriving community of likeminded people who seek simplicity and clarity in a complex world, writing web logs and serving them over gopher and gemini. The best static site generator is still hugo, but there are only few documents starting with it from scratch, rather than existing templates.
We’re setting up Hugo from scratch in these notes, without external themes and just go through the basics. Keeping the layouts simple allows for a later addition of providing the same content as a Gemini gemlog without making the two versions visually too different.
At the time of writing, the homebrew version of Hugo does us no good, neither does the upstream version since it cannot compile Math formulas at compile time. KaTeX is a math typesetting library that produces the same output regardless of browser or environment, allowing to pre-render expressions and serve them as plain HTML. Perfect for Hugo. The corresponding pull request of adding KaTeX was rejected due to the CGO requirement (understandably so). You can download my hugo patch n0x1m/hugo-katex-patch. I try to keep it reasonably up to date and only changes ~10 LOC.
git clone https://github.com/gohugoio/hugo.git
cd hugo
git fetch -a
# v0.92.2 here or latest tagged version, it may work too
git checkout v0.92.2
git apply 0001-PATCH-goldmark-add-katex-extension-support.patch
# instal hugo
go mod tidy
go build
which hugo
/usr/local/bin/hugo
mv hugo /usr/local/bin/hugo
Enable Katex in the hugo config toml:
[markup]
defaultMarkdownHandler = "goldmark"
[markup.goldmark.extensions]
katex = true
If you don’t care about math, just go ahead and install Hugo with homebrew.
Create the site skeleton
hugo new site mylog
cd mylog
the file tree looks like this
ll .
├── archetypes
│ └── default.md
├── config.toml
├── content
├── data
├── layouts
├── static
└── themes
The hugo serve
command will still crash on visit with
hugo serve
WARN 2021/06/14 15:58:23 found no layout file for "HTML" for kind "home": You should create a template file which matches Hugo Layouts Lookup Rules for this combination.
add a basic html layout
mkdir -p layouts/_default
mkdir -p layouts/partials
mkdir -p content/post
touch layouts/404.html
touch layouts/index.html
touch layouts/_default/baseof.html
touch layouts/_default/list.html
touch layouts/_default/single.html
touch layouts/partials/head.html
touch layouts/partials/style.html
touch archetypes/default.md
touch content/_index.md
touch content/posts/sample-post.md
The core html component is layouts/_default/baseof.html
, a basic version looks like this:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{{ .Title }}</title>
</head>
<body>
<div class="container">
<main id="main">
{{ block "main" . }}{{ end }}
</main>
</div>
</body>
</html>
create `layouts/_default/single.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{{ .Title }}</title>
</head>
<body>
<div class="container">
<main id="main">
<h1>{{ .Title }}</h1>
{{ .Content }}
</main>
</div>
</body>
</html>
the archetype ./archetypes/default.md
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: false
---
configure our link format in config.toml
:
[permalinks]
posts = "/:year/:month:day/:filename"
now we can import/add content under content/posts/___.md
Running hugo will render the markdown files by their title in your URL bar. This is the minimum required for a simple page. Styling and layouting can be done of course to greater extend. But I don’t need to list this here as there are plenty or resouces out there.
This is just the base setup conceptually, from here onwards it’s just styles and content. I’ll let you play withthat on your own time.
We don’t want search engines to classify us as a link farm, so let’s make sure we render hugo pages with the nofollow tags (on the html version).
[markup]
defaultMarkdownHandler = "blackfriday"
[markup.blackFriday]
nofollowLinks = true
hrefTargetBlank = true
That’s all for now. If anything is unclear and you send me questions I might add to this post.
The above was a simple way when using the blackfriday Markdown renderer which deprecates soon with
the next Hugo versions. Version hugo v0.87.0
starts warning about this deprecation.
Render hooks allow us several ways to extend the default markdown behaviour, e.g. resizing images, or adding rel=nofollow tags.
mkdir -p layouts/_default/_markup/
touch layouts/_default/_markup/render-link.html
And edit said file layouts/_default/_markup/render-link.html
:
<a href="{{ .Destination | safeURL }}" {{ with .Title }} title="{{ . }}" {{ end }}{{ if strings.HasPrefix .Destination "http" }} target="_blank" rel="nofollow" {{ end }}>{{ .Text | safeHTML }}</a>
That’s it.
Allow to automatically jump to headings and subheadings with anchors:
[markup]
defaultMarkdownHandler = "goldmark"
[markup.goldmark.parser]
autoHeadingID = true
autoHeadingIDType = 'github'
JSON-LD is a way to add semantic mark up your output with schema.org
objects.
JSON-LD is a lightweight Linked Data format. It is easy for humans to read and
write and is supposedly an ideal data format for programming environments and
web services. It is an important concept in
SEO. We start by adding a Hugo partial:
touch layouts/partials/site_schema.html
We’re creating the BlogPosting type and its attributes. There is also an article by Google in Advanced SEO.
{{ if .IsHome -}}
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "WebSite",
"name": "{{ .Site.Title }}",
"url": "{{ .Site.BaseURL }}",
"description": "{{ .Site.Params.description }}",
"thumbnailUrl": "{{ .Site.Params.logo | absURL }}",
"license": "{{ .Site.Copyright }}"
}
</script>
{{ else if .IsPage }}
{{ $author := or (.Params.author) (.Site.Author.name) }}
{{ $org_name := .Site.Title }}
{{ $image_path := .Params.thumbnail | default site.Params.image -}}
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "BlogPosting",
"articleSection": "{{ .Section }}",
"name": "{{ .Title | safeJS }}",
"headline": "{{ .Title | safeJS }}",
"description": "{{ if .Description }}{{ .Description | safeJS }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ end }}{{ end }}",
"inLanguage": {{ .Site.LanguageCode | default "en-us" }},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "{{ .Permalink }}"
},
"author" : {
"@type": "Person",
"name": "{{ $author }}"
},
"creator" : {
"@type": "Person",
"name": "{{ $author }}"
},
"accountablePerson" : {
"@type": "Person",
"name": "{{ $author }}"
},
"copyrightHolder" : "{{ $org_name }}",
"copyrightYear" : "{{ .Date.Format "2006" }}",
"dateCreated": "{{ .Date.Format "2006-01-02T15:04:05.00Z" | safeHTML }}",
"datePublished": "{{ .PublishDate.Format "2006-01-02T15:04:05.00Z" | safeHTML }}",
"dateModified": "{{ .Lastmod.Format "2006-01-02T15:04:05.00Z" | safeHTML }}",
"publisher":{
"@type":"Organization",
"name": {{ $org_name }},
"url": {{ .Site.BaseURL }},
"logo": {
"@type": "ImageObject",
"url": "{{ .Site.Params.logo | absURL }}",
"width":"32",
"height":"32"
}
},
"image": {{ $image_path | absURL }},
"url" : "{{ .Permalink }}",
"wordCount" : "{{ .WordCount }}",
"keywords" : [ {{ range $index, $keyword := .Params.categories }}{{ if $index }}, {{ end }}"{{ $keyword }}" {{ end }}]
}
</script>
{{ end }}
I’ve a follow up post on how to setup Gemini alongside
Hugo with minimum hassle. It uses the
same links, content and file structure just on the gemini://
protocol.
Over at Google’s PageSpeed Insights we score 100/100 with our Hugo static site.
The hugo docs point to a number of different comment options. Disqus carries however bloat and advertisements that we don’t want to promote. I don’t know yet if comments are even used and this is one place where I admittedly would like something off the shelf that gets rid off spam and that I don’t need to self host. After looking into it (no more than 5 minutes), the easiest option seems to me to be utteranc.es. I’ve set up an empty repository on GitHub n0x1m/nox.im-comments and embedded a conditially loaded blob based on a button click and if there is Javascript in the browser. Putting comments behind a button click also prevents the section to be abused for 3rd party links, stealing domain authority.
<center id='have-js' style="display: none;">
<br />
<button id='load-comments-btn' onclick="loadComments()"><b>Load comments</b><br /> <small>(requires Javascript via GitHub Utterances)</small></button>
<div id='comments'></div>
<br />
</center>
<script type="text/javascript">
// unhide button if we have javascript
var haveJs = document.getElementById("have-js");
haveJs.setAttribute('style', 'display: block;');
// only load this sh*t on click
function loadComments() {
console.log('loading utterances...')
var btn = document.getElementById("load-comments-btn");
btn.setAttribute('style', 'display: none;')
var anchor = document.getElementById("comments");
var s = document.createElement('script');
s.type = 'text/javascript';
s.src = 'https://utteranc.es/client.js';
s.setAttribute('repo', 'n0x1m/nox.im-comments');
s.setAttribute('issue-term', 'pathname');
s.setAttribute('label', 'utterances');
s.setAttribute('theme', 'preferred-color-scheme');
s.setAttribute('crossorigin', 'anonymous');
anchor.appendChild(s);
}
</script>
This means Javascript and the 3rd party isn’t imposed on any user by default but requires Javascript to be enabled and a deliberate button click. Let’s see where this goes. Happy commenting!