Making a Semantic Blog Theme
published:
signed with 0x683A22F9469CA4EBI am a big fan of the semantic web, POSH markup and microformats. Now that I’m self-publishing again, one of my first tasks is ensuring my site generation templates produce well structured, and semantically rich documents.
It’s important to me that the site work everywhere, especially text browsers and screen readers. Personally, I do at least half of my web browsing through a terminal browser, and all of my subscribed content is consumed through newsboat. It’s dumb when text-based content needs a javascript capable browser or plugins to display text.
Outlining the content
Obviously we all know what a blog is (you’re literally reading one right now). In terms of web semantics though, it’s helpful to think about the structure more formally. So let’s break down exactly what this content is and what it’s for.
Posts
The primary type of content for this blog is a “post”. The post has two core elements:
- title
- body
These are the things we care most about, and without these the post is pretty useless.
Posts can also have some useful secondary data. Things like:
- author
- date of publication
- date of updates
- categorization tags.
The h-entry format is well suited for posts in this format, so that’s what I’ll be using.
Example markup for a post:
<article class="h-entry">
<header>
<h1 class="p-name"><a href="/some/permalink/to/this/post" class="u-url">Post title</a></h1>
<p>
Published by <a class="p-author" href="/about">Mark</a>
on <time class="dt-published" datetime="2019-01-01 12:00:00">January 1, 2019</time>
</p>
<nav>
<ul>
<li><a href="/tags/1" rel="tag">tag1</a></li>
<li><a href="/tags/2" rel="tag">tag2</a></li>
<li><a href="/tags/3" rel="tag">tag3</a></li>
</ul>
</nav>
</header>
<div class="e-content">
<p>This is some post content</p>
</div>
</article>
Pages
Pages are like posts without the secondary data. They are functionally standalone and without any temporal context. As such, they will also use the h-entry microformat.
In the case of the About page, there’s also an embedded h-card for my contact info. This h-card will serve as an authoritative semantic reference for any p-author
elements in other content.
Example markup for a page:
<article class="h-entry">
<header>
<h1 class="p-name"><a href="#" class="u-url">Page title</a></h1>
</header>
<div class="e-content">
<p>This is some page content</p>
</div>
</article>
Feeds
Feeds (in the context of this blog) are just ordered collections of posts. There are two types of feeds:
- posts by date
- posts by tag
Each will be rendered as an HTML page, as well as an RSS feed.
Markup-wise the feed is more conceptual. It will literally just be a <main></main>
tag inside the HTML body, filled with repeating post markup.
Creating the Hugo Theme
As mentioned elsewhere, I use the hugo static site generator. Hugo is fast, flexible, and generates sitemap and rss feeds so I don’t have to. The docs are straight forward enough that I’m not going to reproduce them here. Instead I’m just going to jump in and start creating the template.
First we run the theme creation command in the shell:
hugo new theme semantic
This generates the following scaffold:
themes/semantic
├── archetypes
│ └── default.md
├── layouts
│ ├── 404.html
│ ├── _default
│ │ ├── baseof.html
│ │ ├── list.html
│ │ └── single.html
│ ├── index.html
│ └── partials
│ ├── footer.html
│ ├── header.html
│ └── head.html
├── LICENSE
├── static
│ ├── css
│ └── js
└── theme.toml
Most of these files are empty placeholders, so we’re just going to delete them:
find themes/semantic -type f -name "*.html" -exec rm '{}' \;
rm -rf themes/semantic/static/*
Now our directory structure looks like this:
themes/semantic
├── archetypes
│ └── default.md
├── layouts
│ ├── _default
│ └── partials
├── LICENSE
├── static
└── theme.toml
Hugo has its own concept of content “types”, which work really well with the approach we’ve taken so far. Earlier we defined two semantic types: pages and posts. Per the docs, we need to make a folder per type inside of layouts
to create these types in hugo:
mkdir -p themes/semantic/layouts/{page,post}
By default, hugo expects a content of a type to be in the type’s named folder. For example a post would be under content/post/some-post-here.md
and would produce a url of yoursite.biz/posts/some-post-here/
. A page would be under content/page/some-page.md
and product a url of yoursite.biz/pages/some-page/
.
In my case, I don’t want to have the page
namespace. I want pages to just be at the site root. For example: https://markhuge.com/about. One way to do this, is to put the page content files at the root of the content
folder, and explicitly set the type in the front matter.
Creating Content Archetypes
Because pages and posts will have differing front matter, it’s useful to create archetype templates for each:
Page Archetype:
---
type: "page"
draft: true
---
Post Archetype:
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
type: "post"
author: "Mark"
tags: []
draft: true
---
Here I’ve hardcoded my name as the author, because I’m the only person contributing to my blog. In the public version of the theme, that line is omitted.
Creating The HTML Templates
As outlined earlier, we have single html pages for page
and post
types, as well as feed
pages with lists of posts. Hugo uses a similar convention of single
templates for individual items and list
templates for feeds.
Base template
The baseof.html
template is the master template for a type. If no baseof.html
is provided in the type subfolder, the type will use the baseof.html
template from the _default
folder. In our case the layout for most of the site is the same across types, so we’re only going to specify _default/baseof.html
:
{{ partial "header.html" . }}
{{ block "content" . }}
{{ end }}
{{ partial "footer.html" . }}
This is a trivially simple template that specifies the inclusion of a header template, a footer template, and a block of code labeled “content”, that will be populated by individual templates below. All this does is orient “whatever is in the ‘content’ block” between the header and footer.
Single Post Template
In the layouts/post/single.html
template, we populate the content block used in the baseof.html
template.
Here we’re using the h-entry microformat we setup earlier. For the most part it’s just replacing values with template variables. In the specific instance of tags
, we use the with function to only include the list of tags if the post has any tags to display:
{{ define "content" }}
<main class="main">
<article class="h-entry">
<header>
<h1 class="p-name"><a href="{{ .Permalink }}" class="u-url">{{ .Title }}</a></h1>
<p>
Published by <a class="p-author" href="/about">{{ $.Param "author" }}</a>
on <time class="dt-published" datetime="{{ .Date.Format `Jan 02 2006` }}">{{ .Date.Format "Jan 02, 2006" }}</time>
</p>
// Only display tags list if tags exist on this post
{{ with .Params.tags }}
<nav class="tags">
Tags:
<ul>
{{ range . }}
<li><a href="{{ `tags/` | absURL }}{{ . | urlize }}" rel="tag">{{ . }}</a></li>
{{ end }}
</ul>
</nav>
{{ end }}
</header>
<div class="e-content">
{{ .Content }}
</div>
</article>
</main>
{{ end }}
List of Posts Template
The list template layouts/_default/list.html
rather than in the post subfolder. This is because it’s also considered the “home” page, and hugo will look for the homepage template in _default
. We’re populating the content block the same as we did in the single.html
template. In this case we’re using the h-feed microformat:
{{ define "content" }}
<main class="main">
<ul class="h-feed">
{{ range where .Pages.ByDate.Reverse "Section" "post" }}
<li class="h-entry">
<h2 class="p-name"><a href="{{ .Permalink }}" class="u-url">{{ .Title }}</a></h2>
<time class="dt-published" datetime="{{ .Date.Format `Jan 02 2006` }}">{{ .Date.Format "Jan 02, 2006" }}</time>
<article class="p-summary">
{{ .Summary }}
{{ if .Truncated }}
<p><a href="{{ .RelPermalink }}">Read More…</a></p>
{{ end }}
</article>
</li>
{{ end }}
</ul>
</main>
{{ end }}
Single Page Template
Pages only have a single layout: layouts/page/single.html
. Just as with posts, we’re populating the content block from the base template. The page template is also an h-entry, but lacks all the secondary data of a post, so it’s much simpler:
{{ define "content" }}
<main class="main">
<article class="h-entry">
<header>
<h1 class="p-name"><a href="{{ .Permalink }}" class="u-url">{{ .Title }}</a></h1>
</header>
<div class="e-content">
{{ .Content }}
</div>
</article>
</main>
{{ end }}
Tags Index Template
Hugo has another type of template called a terms template. A “term” is a type of category. In our case we’re using tags
, so the term template will be what renders our list of tags. This will be in the _default
layout directory: layouts/_default/terms.html
.
Notice this template does not make use of baseof.html
, so we need to include the header.html
and footer.html
partials directly:
{{ partial "header.html" . }}
<main class="main">
<h1>Tags</h1>
<ul class="taglist">
{{ $data := .Data }}
{{ range $key,$value := .Data.Terms.ByCount }}
<li>
<a href="{{ (print $data.Plural "/" ($value.Name | urlize) "/") | relURL }}">
{{ $value.Name }}
</a>
<strong>
{{ $value.Count }}
</strong>
</li>
{{ end }}
</ul>
</main>
{{ partial "footer.html" . }}
Header partial template
The layouts/partials/header.html
template is everything above the content block.
Of particular interest here is the generation of style.css
from a sass file. See “Creating the Stylesheets” further down.
The theme expects normalize.css to be in the static
folder of your hugo site. I did this because I’m not sure what’s appropriate license-wise if I were to ship the theme with normalize included.
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<title>{{.Title}}</title>
{{ $options := (dict "targetPath" "style.css" "outputStyle" "compressed") }}
{{ $style := resources.Get "sass/main.sass" | resources.ToCSS $options }}
<link rel="stylesheet" type="text/css" href="{{ $style.Permalink }}">
<link rel="stylesheet" type="text/css" href="/normalize.css">
<link rel="icon" href="/favicon.ico">
{{ range .AlternativeOutputFormats -}}
{{ printf `<link rel="%s" type="%s" href="%s" title="%s" />` .Rel .MediaType.Type .Permalink $.Site.Title | safeHTML }}
{{ end -}}
</head>
<body>
<header class="main">
<h1 class="logo"><a href="/"><img src="{{ .Site.Params.logo }}" alt="{{ .Site.Title }}"/></a></h1>
<nav class="menu">
<ul>
<li><a href="/about">About</a></li>
<li><a href="/">Blog</a></li>
<li><a href="/tags">Tags</a></li>
<li><a href="https://git.markhuge.com">Git</a></li>
<li><a href="https://git.markhuge.com/releases">Releases</a></li>
<li><a href="/index.xml"><img src="/images/rss.svg" alt="rss" class="rss" /></a></li>
</ul>
</nav>
</header>
Footer partial template
The layouts/partials/footer.html
template is everything below the content block.
Here we’re making use of a copyright
param that is set in the site’s config file. This is a string that represents whatever name you want to copyright your content under. In my case it’s my name, but for you it could be a company or something else.
<footer class="main">
<hr>
<small>
© {{ now.Format "2006" }} {{ .Site.Params.copyright }} | <a href="/sitemap.xml">Sitemap</a>
</small>
</footer>
</body>
</html>
Creating the Stylesheets
I’m using sass because I want to, and hugo supports it out of the box.
One of the benefits of starting with POSH markup, is that we don’t need to do a ton of work to style it. We’re not abusing an endless series of <div>
tags to place things on the screen. Content is already flowing in a reasonable and structured fashion. All the CSS needs to do is format the typeface, colors, and define the constraints for width on different devices.
Typeface
I really prefer to de-Google whenever possible, but I think it’s worse to ship fonts directly.
Here I choose a readable font for regular text, and a readable font for code. Both have reasonable fallbacks:
@import url("https://fonts.googleapis.com/css?family=Inconsolata|Open+Sans");
$fontCodeFace: 'Inconsolata', monospace
$fontTextFace: 'Open Sans', sans-serif
// colors
$menuLink: black
$menuHover: blue
body
font: 100% $fontTextFace
pre
font: 100% $fontCodeFace
padding: 1em
overflow: scroll
Next is layout:
.main
max-width: 80%
margin: 0 auto
header.main
display: flex
justify-content: space-between
border-bottom: 2px solid #000
footer.main
margin-bottom: 1em
font-size: 1em
hr
border: 1px solid black
img.rss
margin: 0
width: 1em
padding: 0px
vertical-align: middle
.logo
margin: 1em 0 5px 0
padding: 0
.p-name
margin: 1em 0 0 0
a
color: black
text-decoration: none
And then some list styling:
.taglist
ul, li
list-style: none
nav.menu
margin: auto 0 5px 0
padding: 0px
vertical-align: bottom
ul
padding: 0px
margin: 0px
li
::after
content: " | "
&:last-child
::after
content: ""
ul,li
display: inline-block
list-style: none
a
text-decoration: none
color: $menuLink
&:hover
color: $menuHover
.tags
ul
padding: 0px
li
::after
content: ", "
&:last-child
::after
content: ""
ul,li
display: inline
list-style: none
.h-feed
ul,li
list-style: none
li
article
margin-top: 1em
As a consequence of simplicity, the theme is mostly responsive by default. There is one place where it makes sense to use a media query, and that’s in the sizing of “hero” images on posts.
figure.hero
max-width: 100%
margin: 1em 0 0 0
padding: 0
img
max-width: 100%
@media (min-width: 1024px)
max-width: 640px
figcaption
font-size: 0.5em
I’m sure there’s a ton of tweaking in this stylesheet’s future, but this is good enough to ship for now.
Conclusion
This is a pretty okay starting point. The terms template will definitely need some work, and the stylesheet will probably get tweaked regularly.
You can download, and track future development of the theme here: https://git.markhuge.com/hugo-semantic-theme
If you’re following along and making your own theme, your completed directory listing should look something like this:
themes/semantic/
├── archetypes
│ ├── default.md
│ ├── page.md
│ └── post.md
├── assets
│ └── sass
│ └── main.sass
├── layouts
│ ├── _default
│ │ ├── baseof.html
│ │ ├── list.html
│ │ └── terms.html
│ ├── page
│ │ └── single.html
│ ├── partials
│ │ ├── footer.html
│ │ └── header.html
│ └── post
│ └── single.html
├── LICENSE
├── README
├── static
│ └── images
│ └── rss.svg
└── theme.toml