markhuge MH\n s

Making a Semantic Blog Theme

published:

signed with 0x683A22F9469CA4EB

tags:

I 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>

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>
          &copy; {{ 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

About the Author

Mark

0x683A22F9469CA4EB

R&D engineer @ Twitch. Previously Blizzard, Hightail, Co-Founder @ SpeakUp

Mark is building open source tools to make web3 and decentralized self-ownership easy for regular people.

Live dev streams at Twitch.tv

More Posts