💾 Archived View for nox.im › posts › 2021 › 0614 › hugo-blog-from-scratch captured on 2023-09-28 at 15:55:43. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2022-06-03)
-=-=-=-=-=-=-
Since the Eternal September[1], 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.
Windows 95 dialing progress - the old internet[1]
1: Windows 95 dialing progress - the old internet
I like the smol internet[1].
Even in the modern web, articles are fundamentally plain text. The Gopher protocol[1] is currently undergoing a renaissance and has spawned some rethinking and the gemini protocol[2].
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[1], 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[1] 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[1] 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[2] was rejected due to the CGO requirement (understandably so). You can download my hugo patch n0x1m/hugo-katex-patch[3]. I try to keep it reasonably up to date and only changes ~10 LOC.
3: hugo patch n0x1m/hugo-katex-patch
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>
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[1]. We start by adding a Hugo partial:
touch layouts/partials/site_schema.html
We're creating the BlogPosting[1] type and its attributes. There is also an article by Google in Advanced SEO[2].
{{ 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[1] with minimum hassle. It uses the same links, content and file structure just on the `gemini://` protocol.
1: setup Gemini alongside Hugo
Over at Google's PageSpeed Insights[1] we score 100/100 with our Hugo static site.
PageSpeed Insights nox.im[1]
The hugo docs point to a number of different comment options[1]. 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[2]. I've set up an empty repository on GitHub n0x1m/nox.im-comments[3] 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[4].
1: point to a number of different comment options
<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!