2019-12-08 23:47:52 -08:00
|
|
|
---
|
|
|
|
title: JavaScript-Free Sidenotes in Hugo
|
|
|
|
date: 2019-12-07T00:23:34-08:00
|
|
|
|
tags: ["Website", "Hugo", "CSS"]
|
|
|
|
---
|
|
|
|
|
|
|
|
A friend recently showed me a website, the design of which I really liked:
|
|
|
|
Gwern Branwen's [personal website](https://www.gwern.net/index). In particular,
|
|
|
|
I found that __sidenotes__ were a feature that I didn't even know I needed.
|
|
|
|
A lot of my writing seems to use small parenthesized remarks (like this), which,
|
|
|
|
although it doesn't break the flow in a grammatical sense, lengthens the
|
|
|
|
sentence, and makes it harder to follow. Since I do my best to write content
|
2019-12-24 15:30:12 -08:00
|
|
|
to help explain stuff (like the [compiler series]({{< relref "00_compiler_intro.md" >}})),
|
2019-12-08 23:47:52 -08:00
|
|
|
making sentences __more__ difficult to understand is a no-go.
|
|
|
|
|
|
|
|
So, what do they look like?
|
|
|
|
{{< sidenote "right" "example-note" "Here's an example sidenote." >}}
|
|
|
|
This is this example note's content.
|
|
|
|
{{< /sidenote >}}
|
|
|
|
If you're on a mobile device, the content is hidden by default: there's no
|
|
|
|
"side" on which the note fits. In this case, you can click or tap the underlined
|
|
|
|
portion of the text, which is the part to which the sidenote is related or refers to.
|
|
|
|
Otherwise, the example sidenote should be visible
|
|
|
|
{{< sidenote "left" "left-note" "on the right side of the screen." >}}
|
|
|
|
Sidenotes can also appear on the left of the screen, to help prevent situations
|
|
|
|
in which there are too many sidenotes and not enough space.
|
|
|
|
{{< /sidenote >}}
|
|
|
|
|
|
|
|
A major goal of mine in implementing these sidenotes was to avoid the use of JavaScript.
|
|
|
|
This is driven by my recent installation of uMatrix. uMatrix is an extension
|
|
|
|
that blocks JavaScript loaded from domains other than the one you're visiting.
|
|
|
|
To my frustration, a lot of the websites I commonly visit ended up broken:
|
|
|
|
[Canvas](https://github.com/instructure/canvas-lms), Discord, YouTube, and
|
|
|
|
Disquss all fail catastrophically when they aren't allowed to load dozens of scripts
|
|
|
|
from various sources. Out of spite, I want my site to work without any JavaScript,
|
|
|
|
and these notes are no exception.
|
|
|
|
|
|
|
|
### Implementation
|
|
|
|
Some of this work has been inspired by
|
|
|
|
[this article](https://www.kooslooijesteijn.net/blog/semantic-sidenotes).
|
|
|
|
The first concern was not having to write raw HTML to add the side notes,
|
|
|
|
but this is fairly simple with Hugo's shortcodes: I write the HTML once in
|
|
|
|
a new `sidenote` shortcode, then call the shortcode from my posts. The next
|
|
|
|
issue is a matter of HTML standards. Markdown rendering generates `<p>` tags.
|
|
|
|
According the to spec, `<p>` tags cannot have a block element inside
|
|
|
|
them. When you _try_ to put a block element, such as `<div>` inside `<p>`,
|
|
|
|
the browser will automatically close the `<p>` tag, breaking the rest of the page.
|
|
|
|
So, even though conceptually (and visually) the content of the sidenote is a block,
|
|
|
|
{{< sidenote "right" "markdown-note" "it has to be inside an inline element." >}}
|
|
|
|
There's another consequence to this. Hugo implements Markdown inside shortcodes
|
|
|
|
by rendering the "inner" part of the shortcode, substituting the result into the
|
|
|
|
shortcode's definition, and then finally placing that into the final output. Since
|
|
|
|
the Markdown renderer wraps text in paragraphs, which are block elements, the
|
|
|
|
inside of the shortcode ends up with block elements. The same tag-closing issue
|
|
|
|
manifests, and the view ends up broken. So, Markdown cannot be used inside sidenotes.
|
|
|
|
{{< /sidenote >}}
|
|
|
|
|
|
|
|
That's not too bad, overall. We end up with a shortcode definition as follows:
|
|
|
|
```HTML
|
|
|
|
<span class="sidenote">
|
|
|
|
<label class="sidenote-label" for="{{ .Get 1 }}">{{ .Get 2 }}</label>
|
|
|
|
<input class="sidenote-checkbox" type="checkbox" id="{{ .Get 1 }}"></input>
|
|
|
|
<span class="sidenote-content sidenote-{{ .Get 0 }}">
|
|
|
|
{{ .Inner }}
|
|
|
|
</span>
|
|
|
|
</span>
|
|
|
|
```
|
|
|
|
As Koos points out, "label" works as a semantic tag for the text that references
|
|
|
|
the sidenote. It also helps us with the checkbox
|
|
|
|
`<input>`, which we will examine later. Since it will receive its own style,
|
|
|
|
the inner content of the sidenote is wrapped in another `<span>`. Let's
|
|
|
|
get started on styling the parts of a sidenote, beginning with the content:
|
|
|
|
|
|
|
|
```SCSS
|
|
|
|
.sidenote-content {
|
|
|
|
display: block;
|
|
|
|
position: absolute;
|
|
|
|
width: $sidenote-width;
|
|
|
|
box-sizing: border-box;
|
|
|
|
margin-top: -1.5em;
|
|
|
|
|
|
|
|
&.sidenote-right {
|
|
|
|
right: 0;
|
|
|
|
margin-right: -($sidenote-width + $sidenote-offset);
|
|
|
|
}
|
|
|
|
|
|
|
|
// ...
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
As you can see from the sidenotes on this page, they are displayed as a block.
|
|
|
|
We start with that, then switch the sidenotes to be positioned absolutely, so that
|
|
|
|
we can place them exactly to the right of the content, and then some. We also
|
|
|
|
make sure that the box is sized __exactly__ the amount in `$sidenote-width`, by
|
|
|
|
ensuring that the border and padding are included in the size calculation
|
|
|
|
using `border-box`. We also hide the checkbox:
|
|
|
|
|
|
|
|
```SCSS
|
|
|
|
.sidenote-checkbox {
|
|
|
|
display: none;
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
Finally, let's make one more adjustment to the sidenote and its label: when
|
|
|
|
you hover over one of them, the other will change its appearence slightly,
|
|
|
|
so that you can tell which note refers to which label. We can do so by detecting
|
|
|
|
hover of the parent element:
|
|
|
|
|
|
|
|
```SCSS
|
|
|
|
.sidenote {
|
|
|
|
&:hover {
|
|
|
|
.sidenote-label { /* style for the label */ }
|
|
|
|
.sidenote-content { /* style for the sidenote */ }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
### Hiding and Showing
|
|
|
|
So far, it's hard to imagine where JavaScript would come in. If you were
|
|
|
|
always looking at the page from a wide-screen machine, it wouldn't at all.
|
|
|
|
Unfortunately phones don't leave a lot of room for margins and sidenotes, so to
|
|
|
|
make sure that these notes are visible to mobile users, we want to show
|
|
|
|
them inline. Since the entire idea of sidenotes is to present more information
|
|
|
|
__without__ interrupting the main text, we don't want to plop something down
|
|
|
|
in the middle of the screen by default. So we hide sidenotes, and show them only
|
|
|
|
when their label is clicked.
|
|
|
|
|
|
|
|
Gwern's site doesn't show the notes on mobile at all (when simulated using Firefox's
|
|
|
|
responsive design mode), and Koos uses JavaScript to toggle the sidenote text. We will
|
|
|
|
go another route.
|
|
|
|
|
|
|
|
This is where the checkbox `<input>` comes in. When the `<input>` checkbox is
|
|
|
|
checked, we show the sidenote text, as a block, in the middle of the page. When
|
|
|
|
it is not checked, we keep it hidden. Of course, keeping a checkbox in the middle
|
|
|
|
of the page is not pretty, so we keep it hidden. Rather than clicking the checkbox
|
|
|
|
directly,
|
|
|
|
{{< sidenote "right" "accessibility-note" "the users can click the text that refers to the sidenote," >}}
|
|
|
|
I'm not sure about the accessibility of such an arrangement. The label is semantic, sure, but
|
|
|
|
the checkbox is more sketchy. Take this design with a grain of salt.
|
|
|
|
{{< /sidenote >}} which happens
|
|
|
|
to also be a label for the checkbox input. Clicking the label toggles the checkbox,
|
|
|
|
and with it the display of the sidenote. We can use the following CSS to
|
|
|
|
get that to work:
|
|
|
|
|
|
|
|
```SCSS
|
|
|
|
.sidenote-content {
|
|
|
|
// ...
|
|
|
|
@media screen and
|
|
|
|
(max-width: $container-width + 2 * ($sidenote-width + 2 * $sidenote-offset)) {
|
|
|
|
position: static;
|
|
|
|
margin-top: 10px;
|
|
|
|
margin-bottom: 10px;
|
|
|
|
width: 100%;
|
|
|
|
display: none;
|
|
|
|
|
|
|
|
.sidenote-checkbox:checked ~ & {
|
|
|
|
display: block;
|
|
|
|
}
|
|
|
|
|
|
|
|
&.sidenote-right {
|
|
|
|
margin-right: 0px;
|
|
|
|
}
|
|
|
|
|
|
|
|
// ...
|
|
|
|
}
|
|
|
|
// ...
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
We put the position back to `static`, and add margins on the top and bottom of the node.
|
|
|
|
We keep the `display` to `none`, unless the checkbox contained in the sidenote span
|
|
|
|
is checked. Finally, we reset the margin we created earlier, since we're not moving this
|
|
|
|
note anywhere.
|
|
|
|
|
|
|
|
### Conclusion
|
|
|
|
Here, we've implemented sidenotes in Hugo with zero JavaScript. They work well on
|
|
|
|
both mobile and desktop devices, though their accessibility is, at present,
|
|
|
|
somewhat uncertain.
|