Martigeon's Notes:

Textbook-themed webdesign

Designing a minimal flat-hierarchy website from scratch with Hugo.

My plan to create a personal website started out, as many others do, on WordPress. I was happy at first, but grew frustrated with its limitations and decided to write a website from scratch, which meant having to understand HTML and CSS at a deeper level than I had to before. I’ll highlight some of my design choices and give a walkthrough of how I implemented it with the help of a fantastic website generator called Hugo.

Design goals

Textbook-theme

I was very much inspired by the layout of some science textbooks, especially Atkins’ Physical Chemistry. I like the use of margins for both images and notes, and that some figures would extend to the margins if their widths justified it. Textbooks also strike a nice balance between information density and readability that I’m going for. I thought it would be a cool idea if the home page gave the impression of a table-of-contents page that you find in the beginning of textbooks, with the first “chapter” being the about page of the website, and the following “chapters” linking to all posts, in order of publication date.

A sample page of the 8th edition of Atkins’ Physical Chemistry textbook.

A sample page of the 8th edition of Atkins’ Physical Chemistry textbook.

Key points:

Using Hugo

Hugo is a static website generator written in Go. Static website generators are especially useful for creating blogs, personal pages, or documentation websites – any website with content that does not change from visitor to visitor. It is a simple method to avoid having to write HTML manually. Hugo seems to be picking up steam; it has a reputation for being extremely fast, and relatively easy to use.

Getting started

Step one is to install Hugo for whatever system we have, described here. Once hugo is in our path, use these command line arguments:

hugo new site textbook
cd textbook

You should now have a directory called textbook with a few subdirectories and one file. I’ll explain the significance of the ones necessary to follow this guide:

Running Hugo

Hugo has a very useful feature where it can run as a real-time server by running the command hugo server from within the root of your project folder (textbook in our case). It will then periodically scan your files and “recompile” your website whenever it detects a change. One of Hugo’s selling points is that this process is remarkably performant. All you need to do is to go to localhost:1313 in your browser and you should see your website, provided you have already written the HTML files.

If you run hugo without server, all files related to your website will be emitted to the directory public. When you’re prototyping and actively editing, it makes more sense to use Hugo as a server.

General layout

We create three HTML files with the following structure:

└── layouts
    ├── _default
    │   ├── baseof.html
    │   └── single.html
    └── index.html

The layouts/_default/ folder is important because Hugo has a lookup order for certain files; putting it in this folder means it will be found last, implying it is the default.

baseof.html

baseof.html plays a special role: it is used to give the outer shell of each page of your website. Any code that is identical in each page should go here.

<!DOCTYPE html>
<html lang="en">
<head>
	<title>{{ if not .IsHome }}{{ .Title }} &middot; {{ end }}{{ .Site.Title }}</title>
	<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
	<link rel="stylesheet" href="{{ relURL `/style.css` }}"/>
</head>
<body>
	<nav><a href="{{ .Site.BaseURL }}"><img src="{{ relURL `/home.svg` }}"></a></nav>
	{{ block "main" . }}{{ end }}
</body>
</html>

Every statement that is wrapped in between double curly braces {{ }} is something that will be handled and executed by Hugo’s templating library. In the <head> tag we have three elements:

  1. The title of the browser toolbar is the post’s title concatenated with the site-wide title given in your config.toml file. If you’re at the home page the post’s title is omitted, along with the concatenation character ·.
  2. A <meta> tag which is to make sure that your phone’s browser won’t try to do it’s own trickery.
  3. The <link> tag references a CSS stylesheet which we have put in static/style.css.

And the <body> tag has the navigation bar which we put at the top with only one image in the middle to link to the site’s home page. The next line is important: {{ block "main" . }}{{ end }}. It tells Hugo to look for a piece of text marked main in other layout files and place it here. That’s what our other two HTML files do. and index.html provides it for the home page.

single.html

single.html provides the main block for a single post page.

{{ define "main" }}
<main class=single>
	<header>
		<div id=sitetitle>{{ .Site.Title }}:</div>
		<h1>{{ .Title }}</h1>
		{{ with .Page.Date }}<time>{{ .Format "2 Jan 2006" }}</time>{{ end }}
		<div id=description>{{ .Param "description" }}</div>
	</header>
	<article class=single>
		<aside id=toc>
			{{ .TableOfContents }}
		</aside>
		<span id=dropcap></span>{{ .Content }}
	</article>
</main>
{{ end }}

The <header> tag (not to be confused with <head>) contains the website title followed by the post’s title. Then there is an optional date element, and a description or subtitle of the post. Inside <article> we first render the table of contents of the page, and wrap it in an <aside> tag so that it will move to the right margin responsively. The next {{ .Content }} statement calls the markdown renderer and converts your content files from markdown to raw HTML. (<span id=dropcap></span> is a hack that I will explain later on.)

index.html

index.html provides the layout for the home page of the website.

{{ define "main" }}
<main>
	<header>
		<div id=sitetitle>{{ .Site.Title }}:</div>
		<h1>Contents</h1>
	</header>
	<article>
		<ol class=home>
		{{ range $index, $page := .Site.RegularPages }}
			<li class=home>
				<div class=number>{{ printf "%02d" $index }}</div>
				<div class=details>
					{{ with $page.Date }}<time class=home>{{ .Format "2 Jan 2006" }}</time>{{ end }}
					<a href="{{ .RelPermalink }}"><h2 class=home>{{ $page.Title }}</h2></a>
					<div class=subtitle>{{ with $page.Param "description" }}{{ . }}{{ end }}</div>
				</div>
			</li>
		{{ end }}
		</ol>
	</article>
</main>
{{ end }}

The <header> tag is fairly similar, but something more interesting happens the <article> tag. range in hugo is a command that will loop a through the elements of a given list, which, in this case, are all the pages in your content directory. Inside each <li> item some custom formatting is done so that we don’t have to rely on the default list-style.

Content files

In the content folder we have the following directory structure:

└── content
    ├── about.md
    └── posts
        └── example.md

The about.md page will be shown first on the home page, and then come the markdown files in posts. Each markdown file has a preamble called front matter which is enclosed by lines with three plug signs +++. (If you want to use YAML instead of TOML, you would use three minus signs ---.) Front matter can be used to set certain variables per content file.

+++
weight = 99
date = "2019-03-11"
title = "About"
description = "Learn about this website and its author."
+++

Now you understand where all these variables are taken from in single.html. The weight variable determines priority of the listing in index.html. So by putting a high number we guarantee that the about page is shown first.

Global settings

Then we have a few more remaining files which much be created in order to have a functioning Hugo website. We put a CSS stylesheet and the home icon in static folder so that it can be properly referenced in baseof.html. We also customize our config.toml and get the following directory structure:

├── config.toml
└── static
    ├── style.css
    └── home.svg

With these settings in our config file:

baseURL = "https://martigeon.github.io/textbook-hugo"
title = "My Website"
publishDir = "docs"

[permalinks]
  posts = "/:title/"

[markup.goldmark.renderer]
  unsafe = true

[markup.highlight]
  style = "pygments"
  tabWidth = 2

Take note of the [permalinks] setting. This is a little trick which makes sure that the URL goes from example.com/posts/file-name/ to example.com/title/ which just makes it a little bit cleaner. The publishDir variable is necessary to get this repository to work with github pages.

Specific features

The exact contents of the style.css file is probably the most interesting, but it’s too large to display here and contains a lot of boilerplate. I will go over some features which I coded into this stylesheet.

Typography

Typography is surprisingly complicated – both in terms of design and rendering technology. (Just look at subpixel rendering, hinting, kerning, or ligatures.) I was initially committed to using a serif font because this more accurately reflects the styling of a textbook, but serif fonts are also more difficult to render readably at small font sizes. Medium.com, most notably, uses a serif font, but their content is also spaced more generously, which I wanted to avoid in the interest of a higher information density.

And then there was the question of whether to go for a web-font or a system font stack. This seems to be quite a contested issue among web developers. Seeing as one of my design goals was minimal dependencies, I thought it made sense to go for a fairly ubiquitous sans-serif font stack:

font: normal 15px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";

This also meant that I would have to use color and styling to differentiate between headings and text instead of relying on font pairings. I did this out of the assumption that it’s probably more difficult to create a consistent design across platforms with two font stacks.

Drop cap hack

By manually placing an empty inline element before the first paragraph, I got a drop cap effect like so:

#dropcap + p:first-letter {
  color: #ce3333;
  float: left;
  font-size: 3em;
  line-height: 0.9;
  padding-right: 3px;
}

Without the table of contents, I could have used this selector: article > p:first-child:first-letter, but this way it gives me the freedom of keeping the table of contents as an optional feature for now.

Responsive design

To make the page layout responsive I created three viewport-width zones where the CSS layout was slightly different.

  1. Small: (below 740px) No side margins and thin padding. This includes pretty much all mobile devices in portrait orientation.
  2. Medium: (between 740px and 1060px) Thicker padding for main text, added auto margins, and drop-shadow paper effect.
  3. Large: (above 1060px) Added right column for margin-notes.

Which affects the <main> and <article> tags according to the following CSS:

main {
  background-color: #fff;
  padding: 50px 20px 100px;
}

@media (min-width: 740px) {
  main {
    padding: 50px 50px 100px;
    margin: 20px auto;
    max-width: 700px;
    border: solid 1px #bbb;
    box-shadow: 0 0 20px #bbb;
  }
}

@media (min-width: 1060px) {
  article.single { margin-right: 300px; }
  main.single { max-width: 1000px; }
}

You can see that in the largest regime the margins are only expanded for single.html because I thought it was unnecessary for the home page.

Figures

Hugo has a feature called shortcodes which is a method of inserting custom HTML snippets whenever markdown is not sufficient. A common one is for figures, and has a syntax like this:

{{< figure src="img.jpg" caption="Caption." class="format" >}}

One of the useful attributes of the shortcode is that you can specify what classes you want your <figure> tag to have with the class parameter. This allows me to customize the formatting of images, especially in relation to the margins.

Classes:

Here is how it is implemented:

figure.border img {
  border: 1px solid #bbb;
}

@media (min-width: 1060px) {
  figure.wide {
    clear: right;
    width: 900px;
  }

  aside, figure.aside {
    float: right;
    clear: right;
    margin-right: -300px;
    width: 270px;
  }
}

Margin notes

I also made a small custom shortcode in the folder layouts/shortcodes/note.html. The name of the HTML file corresponds to the name you will use to call it.

<aside>
	{{ with (.Get "title") }}<h4>{{ . }}</h4>{{ end }}
	{{ .Inner | markdownify }}
</aside>

It’s a little different from figure in that it has opening and closing brackets. This is because I often want to use normal, multi-line markdown syntax when creating a margin-note, so this is an easy way of doing that.

{{< note title="Optional title" >}}
> You _can_ use **markdown** syntax here.
{{< /note >}}

Designing it like this also means that these notes will function like block elements, seeing as they are enclosed in <aside> tags. If you put one inside a <p> tag, chrome will interpret that as an error and prepend a closing </p> tag to the margin-note, meaning that this will always break the paragraph. I decided that I wanted these notes to act as something more substantial than a footnote, so I just use them with that in mind. My implementation also differs from the typical Tufte style in that the box is always shown, even on mobile – it acts identical to a figure with aside set as its class.

Remarks

I think it turned out pretty well for only three layout files (+ a shortcode file) and some CSS. Now that I’ve done this I can’t help but feel that the vast majority of static webpages are unnecessarily overengineered. Although, full disclosure, using a WordPress template would have saved me many days of not having to learn the nuances of web design. I highly recommmend anyone getting into this to really put a lot of effort in understanding how HTML elements flow and layout. A good resource is learnlayout.com.

There’s a lot of room for improvement, of course. For example, I have not put any thought into optimizing each page for search engines (SEO). Subtle things like a favicon, too. Next steps.

Demo github page: here.

Web design inspiration: