From 4880f641ade11e60f18f907fdf0660ca349714a7 Mon Sep 17 00:00:00 2001 From: Prefetch Date: Tue, 15 Nov 2022 22:13:52 +0100 Subject: Publish "Website adventures" part 1 about static site generators --- source/blog/2022/things-i-use/index.md | 11 +- .../2022/website-adventures-generators/index.md | 474 +++++++++++++++++++++ 2 files changed, 481 insertions(+), 4 deletions(-) create mode 100644 source/blog/2022/website-adventures-generators/index.md (limited to 'source') diff --git a/source/blog/2022/things-i-use/index.md b/source/blog/2022/things-i-use/index.md index 65247c5..db163eb 100644 --- a/source/blog/2022/things-i-use/index.md +++ b/source/blog/2022/things-i-use/index.md @@ -11,7 +11,7 @@ so I've made a list of the programs I like enough to recommend. Such a list has been on my website for a long time already; this is its official publication. -Last updated on 2022-09-28. +Last updated on 2022-11-11. ## General @@ -103,12 +103,15 @@ Last updated on 2022-09-28. It has lots of advanced features that I barely understand, but still seems to be the most modern and usable spam filter out there. * [Zola](https://www.getzola.org/): - Straightforward static site generator written in Rust. - The only thing it's missing is some kind of LaTeX formula support, - which is why I migrated to Hugo. + Static site generator written in Rust. + It's fast, flexible and stays out of your way. * [Hugo](https://gohugo.io/): Another good static site generator, although not quite as nice as Zola in my opinion, since Hugo's template language is a bit messed up. It still works well though. +* [Jekyll](https://jekyllrb.com/): + Yet another static site generator, in Ruby this time. + It's very popular for good reason, + and has a wealth of plugins if you need extra features. * [cgit](https://git.zx2c4.com/cgit/about/): JavaScript-free online Git frontend, perfect for private setups. diff --git a/source/blog/2022/website-adventures-generators/index.md b/source/blog/2022/website-adventures-generators/index.md new file mode 100644 index 0000000..0aba183 --- /dev/null +++ b/source/blog/2022/website-adventures-generators/index.md @@ -0,0 +1,474 @@ +--- +title: "Adventures in making this website:
static site generation" +date: 2022-11-15 +layout: "blog" +toc: true +--- + +Making and managing this personal website has been an adventure. +In this series, I go over the technical challenges I've encountered +and philosophical decisions I've made, +and I review some of the tools I've used along the way. +This is part 1, with more posts coming soon. + + + +If you've ever taken a good look at HTML, +it should be clear that it isn't fun to write it all by hand. +Therefore, we invented the Static Site Generator (SSG): +a program that takes an HTML template and some text +in a more human-friendly form (usually Markdown), +and mashes them together, yielding a servable HTML file. +Of course, it's pretty clear what the best approach to do this is, +so we only need one SSG, right? + + + + + +Yeah... no, there are [a lot of them](https://jamstack.org/generators/). +Frustratingly many, in fact. +Although some of them are highly specialized or advanced, +most are very similar in scope. +The three SSGs I'm about to discuss +are all intended for personal blogs, +and for many people, there isn't much reason to prefer one over the other. +My SSG-hopping was due to my niche requirements and perfectionism. + + + +## Zola + +The first SSG I used was [Zola](https://www.getzola.org/), +which was started in late 2016 (making it the youngest SSG here), +and advertizes itself as a *"one-stop static site engine"* +that *"gets out of your way so you can focus on your content"*. +Written in Rust, it's fast and portable, +and has convenient features like code syntax highlighting, +support for multilingual sites, and an optional theme library. +Similar to other SSGs, you can run `zola serve` to start a local HTTP server +to test a webpage, which reloads itself each time you make changes; +this makes development a breeze. + + + +### Demonstration + +An example can say more than a thousand words, +so, to give an idea of what it's like to use Zola (other SSGs are similar), +consider this HTML template, +powered by [Tera](https://tera.netlify.app/): + +```html +{% raw %} + + Example + + + {% block content %} + {{ section.content | safe }} + {% endblock content %} + +{% endraw %} +``` + +This is a standard HTML document, except for the parts in curly braces: +`{% raw %}{% %}{% endraw %}` is for statements, +and `{% raw %}{{ }}{% endraw %}` inserts the value of an expression into the output. +We've created a block `content`, and given it the default definition +`{% raw %}{{ section.content | safe }}{% endraw %}`, +where `section.content` is the content of this "section" +after Markdown-to-HTML conversion +(Zola distinguishes *sections* and *pages* based on folder structure and file name). +Then `safe` tells Tera that the string has been fully converted to HTML, +so no more escaping is necessary. + +The point of defining such blocks in the template +is that you can override them. +We thus create a page template which inherits the above structure +(`{% raw %}{% extends "default.html" %}{% endraw %}`) +and redefines the `content` block. +This example also highlights Zola's neat table-of-contents generation: + +```html +{% raw %}{% extends "default.html" %} +{% block content %} +
{{ page.title }}
+

+

+

+ {{ page.content | safe }} +{% endblock content %}{% endraw %} +``` + +Okay, enough templates, what if we actually want to write something? +Well, then we just create a Markdown file in the appropriate place +(I'm being vague, since this isn't intended to be a tutorial) +with contents like: + +```markdown ++++ +title = "Example page" ++++ + +# Heading 1 + +Lorem ipsum dolor sit amet... +``` + +The part surrounded by `+++` is called the *front matter*, +and is a common feature in SSGs, allowing you to set parameters +that can be used by templates or the SSG itself. +And that's it! If we now run `zola build`, +our Markdown will be converted to HTML and inserted into the template. + + + +### Review + +Zola's opinionated design pays off: +it's quite simple, fast and as flexible as your templates allow. +Speaking of which, of the SSGs I've tried, +Tera is in my opinion the cleanest and most powerful template engine. +Zola's main weakness is its obscurity: +if you need help or want to do complex things, +there aren't many resources available other than the official documentation. +But I also think its design is good enough +that you can probably figure it out. + +So, why did I stop using Zola? Due to a giant misunderstanding, that's why. +In late 2020 I started my [knowledge base](/know/), +for which I needed the ability to show maths formulas. +There exist several solutions to put LaTeX maths on websites, +but back then I didn't understand how they worked and what their prerequisites were, +so I mistakenly concluded that it wasn't possible with Zola. +Indeed, Zola doesn't have built-in support for it, +but it turns out I didn't need that! +And I only figured that out while writing this post... + +I'll go over my maths rendering journey in a future article. +In the end, my misguided decision to migrate from Zola did have some benefits; +otherwise my current offline solution wouldn't have been possible. +But in hindsight, I wish I'd spent more time with Zola, +and I think it would be my first recommendation to a beginner. + + + +## Hugo + +I migrated to [Hugo](https://gohugo.io/), an SSG written in Go, +which also markets itself as being fast and flexible, +and is pretty much feature-equivalent to Zola. +Hugo is much more popular than Zola; +in fact, it seems to be one of the most-used SSGs out there. + +The reason I chose it is that it allows page contents to be written in one +of [several formats](https://gohugo.io/content-management/formats/#list-of-content-formats), +one of which is [pandoc's](https://pandoc.org/) Markdown dialect +that has built-in support for LaTeX maths. +So when Hugo is building the site, it passes my `.pdc` file to the external `pandoc` program, +which converts it to HTML to insert into the template. +With how Hugo sets this up, the result is that all maths +get wrapped in HTML `` tags with certain classes, +which I (wrongly) thought was necessary. + + + +### Demonstration + +The previous page template example would look like this in Hugo, +after a few small tweaks like adding a navigation bar at the top of the page: + +```html +{% raw %} + + {{ .Title }} | {{ .Site.Title }} + + + {{ partial "navbar.html" . }} +
{{ .Title }}
+ {{ .TableOfContents }} + {{ .Content }} + +{% endraw %} +``` + +On the one hand, Hugo provides the convenient `.TableOfContents` variable +at the cost of control, unlike Zola, where we had to do it manually. +On the other hand, Hugo doesn't use block-based templates +and hence lacks a nice inheritance system like Zola's. +If you want to reuse a snippet, you put it in a file e.g. `navbar.html` +and include it as `{% raw %}{{ partial "navbar.html" . }}{% endraw %}`. +Note the dot at the end... I'll get to that. +Overall, Hugo's approach to organizing templates feels "dumber" than Zola's, +although, to be fair, my website had become a lot more complicated by then. + +Like most SSGs, Hugo uses an external template engine, +namely the [`text/template`](https://pkg.go.dev/text/template) Go package. +Disclaimer: I'm not a Go developer, so I don't know how nice this library is for various use-cases; +here I'm just reviewing whether it's good for HTML templates in Hugo. + +Okay, here comes my review: it's awful. No, it isn't a good fit here: +the whole point of an SSG is to have a pretty sophisticated template language +to avoid needing to program a custom solution. +This package is just too primitive, +so Hugo has shoehorned all sorts of extensions into it to make it usable; +the result is an opaque mess with still too little flexibility. +For simple stuff like inserting the site's title somewhere it's fine, +but I need some pretty non-trivial templates. + +To give you a taste, consider the following snippet I wrote, +which lists the 10 most recent articles from newest to oldest, +as seen on my knowledge base' [front page](/know/): +```html +{% raw %}

Most recent articles: +

    + {{ $concepts := where .RegularPagesRecursive "File.Dir" ">" "know/concept/" }} + {{ range first 10 $concepts.ByPublishDate.Reverse }} +
  1. {{ .Title | title }}
  2. + {{ end }} +
+

{% endraw %} +``` + +This example captures the messiness of Hugo templates. +But on a more serious note, +there are some questionable design decisions here, +in no particular order: + +* `.Title` is a variable, whose value is implicitly set by the surrounding `range` loop. + And what if you want to insert *this* page's title? + You also write `.Title`, just not inside a loop. + So you don't know what some variables contain, + unless you pay close attention to the context: + you need to be aware that this is happening inside a loop over an array of pages... + and not, of course, an array of something else, + so don't forget to type-check the loop. + + To be fair, what's going on here is clearly explained + [in the docs](https://gohugo.io/templates/introduction/#the-dot): + the initial dot refers to the current context, + of which `Title` is an attribute. + The keywords `range` and `with` swap out the context from under your feet, + which I insist is bad design. + If you need the global context, it can always be accessed explicitly using `$.`, + so at least there's that? + +* Meanwhile, `.ByPublishDate` is a function that sorts a list of pages; + specifically, it's a *method* in the object-oriented sense. + Okay, this works, but why did they add this feature, + instead of just telling us to use the more general + [`sort`](https://gohugo.io/functions/sort/) function? + + `.Reverse` is another method, which flips the order of an array. + I have the same question: why this, instead of a general `reverse` function? + As of writing, no such function exists, + so if you want to reverse an array... [have fun](https://discourse.gohugo.io/t/reverse-array/28753). + + This awkward split between methods and "normal" functions + makes it unclear how things can be composed. + Clearly, you can chain methods as `$xs.ByPublishDate.Reverse`, + or pipeline functions as `$x | title | sha256`, + but how do you mix the two? + And what if we need to pass additional arguments, + e.g. for [`.Param`](https://gohugo.io/functions/param/) + or [`hmac`](https://gohugo.io/functions/hmac/) (WTF, this SSG can do crypto)? + I'm sure there exist answers to these questions; + my point is that it's unnecessarily confusing. + +* Nitpick: for looping over an array, + why did the Go developers choose the keyword `range`, + instead of `for` like almost all other computer languages? + To me, `range` is a function (like in Python), + and the order of `range first 10 xs` hence looks weird (coming from Haskell). + + And why do [Hugo's docs](https://gohugo.io/functions/range/) + claim that `range` is indeed a function, + while the Go [package docs](https://pkg.go.dev/text/template#hdr-Actions) + clearly say that it's an *action*? + Documentation should be precise! + +Maybe, if you're a Go/Hugo veteran, this all just makes sense to you +(then I'm both jealous and scared of you). +I just want you to keep my comments in mind +when I compare the same piece of templating code in the next SSG. + + + +### Review + +I don't mean to shame Hugo's developers with my complaints. +I suspect it started as a private experiment +(which would explain their choice of Go's template package for quick development) +that became too popular too fast. +Looking at [Jamstack's ranking](https://jamstack.org/generators/) of SSGs, +we see that Hugo is the only compiled program anywhere near the top, +and I think that explains its popularity: +there are a lot of performance-focused programmers out there, +who don't want an SSG in an interpreted language like JS or Python. +So when Hugo was released, it quickly attracted many users, +and also many contributors wanting various features. +The result is its current messy feel. + +But for all my complaining, Hugo's popularity is a big advantage, +with many tutorials available online in addition +to the extensive official documentation. +And, as it claims, Hugo is indeed fast and flexible. +In practice, you only really interact with an SSG when writing your templates, +and after that most of your time is spent on actual content, +so although Hugo may sometimes get in your way during development, +it might not be a problem for you. + +Towards the end of 2022, my maths rendering solution required +fetching 450KB of compressed JS on page load, +leading to a visible delay, which I wasn't happy with. +I'd discovered it was possible to render maths offline, +so no JS would need to be loaded, +but this couldn't reasonably be done with Hugo, +unless I forked it like [this guy](https://graemephi.github.io/posts/server-side-katex-with-hugo-part-2/) +did... No thanks. +It was time to change. + + + +## Jekyll + +Compared to Zola and Hugo, +[Jekyll's](https://jekyllrb.com/) feature set is fairly basic: +things like an automatic table of contents or multilingual pages +need to be added using plugins. +But on the upside it supports plugins at all, +thanks to being written in Ruby, an interpreted programming language, +unlike its compiled competitors. +It's quite mature compared to other popular SSGs, +and is what I'm using right now, +since I was able to get statically-rendered maths thanks to a plugin. + + + +### Demonstration + +At first sight, Jekyll's [Liquid](https://shopify.github.io/liquid/) templates +look very similar to Hugo's, due to its inclusion system: + +```html +{% raw %} + + {{ page.title }} | {{ site.title }} + + + {% include navbar.html %} + {{ content }} + +{% endraw %} +``` + +But where did our table of contents go? For that, we need +the [`jekyll-toc`](https://github.com/toshimaru/jekyll-toc) extension +and pipe all our content through the `toc` filter. +Compared to Hugo, Jekyll has a trick up its sleeve here: +you can nest templates inside the `{% raw %}{{ content }}{% endraw %}` object. +So, let the above `default.html` be a skeleton for the actual page template, +which we then define as: + +```html +{% raw %}--- +layout: "default" +--- + +
{{ page.title }}
+ +{{ content | toc }}{% endraw %} +``` + +Another template could be nested in `{% raw %}{{ content }}{% endraw %}`, and so on, +until the innermost block is filled with the converted Markdown, +which, unlike for Zola and Hugo, can also contain template code. +This approach is brilliant in my opinion: it's more flexible than Hugo +and needs less boilerplate than Zola. +Also, Jekyll doesn't try to guess which template should be used for what. + +A standard feature of Markdown is that you can write HTML directly, +which gets copied verbatim while the rest is converted to HTML too. +A minor annoyance is that [kramdown](https://kramdown.gettalong.org/), +Jekyll's converter, insists on parsing the HTML I write, +but isn't very good at it: +for the image on [this page](/blog/2022/email-server-revisited/#motivation-1), +why did it generate a closing `` tag? +That isn't valid! Fortunately, modern browsers are accustomed to this, +so it gets interpreted correctly anyway. + +And what about my snippet to list the 10 most recent articles? +In Jekyll it looks [like this](/code/prefetch-jekyll/tree/source/know/index.md): + +```html +{% raw %}{% assign concepts = site.pages | where_exp: "item", "item.layout == 'concept'" %} +{% assign newest = concepts | sort: "date" | reverse | slice: 0, 10 %} +

Most recent articles: +

    + {% for item in newest %} +
  1. {{ item.title }}
  2. + {% endfor %} +
+

{% endraw %} +``` + +There, isn't that much better? +Yes, it's a bit more verbose than Hugo, +but at least it's pretty clear what's going on; +with the documentation's help, you could write this yourself. +Now it's bearable for me to write complex templates... +although sometimes I maybe take it +[a bit too far](/code/prefetch-jekyll/tree/source/_includes/image.html). + + + +### Review + +Jekyll has been consistently popular for many years, and I can see why. +Its templating system is elegantly organized and flexible enough for most use-cases, +it's infinitely extensible with plugins +and plenty fast despite being written in Ruby. +Although 5 years older than Hugo, it doesn't feel nearly as messy. + +Jekyll deserves some criticism for how it wants you to organize your files by default: +in both Zola and Hugo, you put your markdown into a dedicated `content` folder, +but in Jekyll you just dump it all directly into the root, +meaning that configuration and content aren't clearly separated, which I don't like. +Fortunately, you can fix this in `_config.yml` by setting `source`. + +I'm happy with my choice to move to Jekyll, +and I don't want to do another migration ever again +(~200 files would need to be converted). +My site feels organized now, +and it builds in seconds, even including the process +of rendering all LaTeX maths into HTML. +In fact, I'm so satisfied that I've even published my +[source code](/code/prefetch-jekyll/tree/)! + + + +## Conclusion + +Making a functioning website isn't as hard as it seems, +but making a *good* one does take time. +Most of that time is spent writing HTML templates and especially CSS, +and realistically, both will need to be tweaked again and again over time. +But, in my humble opinion, it's worth the effort: +it's a constructive activity with tangible results. +I like seeing my work appear in a browser. + +Do you want to build your own website using an SSG? +I recommend that you look at Zola first, with Jekyll a close second, +if you can deal with its higher complexity. +Personally, compared to those two, I wouldn't recommend Hugo, +but for some people (probably Go developers) +it just might be what they're looking for. + -- cgit v1.2.3