How To Use Tiddlywiki as a Static Site Generator

2022-02-08

There are a lot of static site generators to choose from, but one that is rarely discussed is Tiddlywiki. I'm a big fan of Tiddlywiki because it's both a product that works out-of-the-box as well as a toolkit that you can use to construct what you need. My Notedeck project is just a handful of extensions on top of Tiddlywiki, and this web site is generated entirely with Tiddlywiki. This post is intended to outline all the steps you need to follow to generate a site just like this one.

Getting Started

Notedeck Saver for Saving

One of the first challenges users of Tiddlywiki face is how to save their wiki as they work on it. Tiddlywiki has a good collection of options based on your setup that you can choose from.

I use Notedeck Saver, a small Python script I've been working on, to handle saving and backups. I haven't released it just yet, mostly because I haven't made it a "product" that I can recommend to others. I'm working on it thought, and will update here when it is available, in case you're curious about it.

Customization

Theme: Notebook by Nico

To get nicely styled and centered content, I use the Notebook theme by Nico. It's extremely well done, and I didn't have to change a thing to get exactly the look and feel I was going for. Thanks so much, Nico!

Removing Post Datelines and Tags

Because the site uses aggregation tiddlers that automatically update based on tags of other tiddlers, the default dateline that appears in Tiddlywiki isn't really representative of the date of the content being shown. To remove the subtitle (author and date), I edited the system tiddler $:/core/ui/ViewTemplate/subtitle to remove the $:/tags/ViewTemplate tag, which is how Tiddlywiki determines what to include when it renders the view.

I also prefer the clean look with tags removed, partly because I use tags on this site to do some behind-the-scenes organization, though there's a strong argument that I could expose that organization as well. Removing tags (or adding them back) is just as simple. I used the exact same approach, except this time I edited the system tiddler $:/core/ui/ViewTemplate/tags to remove the $:/tags/ViewTemplate.

Plugin: Relinking Automatically with relink

I highly recommend the plugin tw5-relink for any Tiddlywiki user. It enables me to refactor the blog with different tag and tiddler names quickly and easily because it finds references to them and updates them automatically.

Plugin: Code Highlighting with highlight.js

highlight.js is one of my favorite libraries for my sites. With Tiddlywiki, there's a highlight plugin that brings the same awesome highlighting functionality to code blocks.

Pick a code theme

Highlight.js allows you to customize the theme it uses by creating a tiddler (you can name it anything you want), setting the type to text/css, and tagging it with $:/tags/Stylesheet.

You can find CSS themes browsing around on the highlight.js demo site. You can download the theme you like directly from the styles directory in highlight.js' GitHub repo.

This site uses a style inspired by Visual Studio's C# theme. You can find it in the file called vs.css.

Organization

Sections: Writing, Projects, Tech Notes, etc.

There are several sections to the site. To make a section, you simply make a tiddler for it. For example, this site has a Tech Notes section. That tiddler is tagged with Section, and has a field called summary that contains a small blurb about what the section contains. Inside the Tech Notes tiddler, there is a bit of introductory text about what it contains, along with a call to the list-links macro:

<<list-links filter:"[tag[Tech Notes]!sort[created]]">>

This pulls in any tiddler that's tagged with Tech Notes and adds it to the list, ensuring that the newest notes are at the top of the list.

All the sections are visible from the index tiddler that's the entry point for the site. To implement this, the index tiddler has a call to the $list widget, which aggregates all the sections with their summaries:

<$list filter="[tag[Section]]">
  <$link/> - {{!!summary}}<br/>
</$list>

This mechanism makes it very easy to add new entries to a section by simply creating a new tiddler with the appropriate tag (Writing, Thoughts, Tech Notes, Projects, or anything else you like). This is particularly easy to accomplish if the wiki is configured to make the "New Here" button visible in the View Toolbar. This button automatically adds a new tiddler tagged with the current one. Similarly, to make a new section, all that's required is creating a new tiddler tagged with Section and adding a field for its summary.

Implementing a "Thoughts" Page

I've long thought that there's some merit to microblogging, particularly if the social aspects are toned down a bit from what the mainstream platforms do. This is not a novel idea! Projects like twtxt and Yarn.social take ideas from popular microblogging products and make them simpler, more distributed, and slower. thoughts takes this a step further, and simply reads in a text file with thoughts and generates a page using some scripts.

This site has a tiddlywiki-flavored take on the same idea. I've given it the name Thoughts, which isn't going to win any awards for originality, I'm afraid, but captures the same gist. Much like Sections described above, this works using a tiddler titled Thoughts that is tagged with Section and has a summary field. And just like sections, the Thoughts tiddler aggregates tiddlers tagged with Thoughts, but rather than only displaying links, it renders in their entirety using the $list widget:

<$list filter="[tag[Thoughts]!sort[created]]">
  <div class="thought">
  <strong><$link/></strong> - <em><$view field="created" format="date" template="YYYY-0MM-0DD" /></em>
  <$transclude mode="block"/>
  </div>
</$list>

This loops over all the tiddlers tagged with Thoughts (ordered in descending chronological order), and creates a div for each that contains a link to the thought, the date it was created, and the content of the thought. The div is given a class thought, which we reference in our CSS style.

To add the CSS, the site has a system tiddler (it's called $:/rpdillon/thought-style, but it can be called anything) of type text/css that's tagged with $:/tags/Stylesheet, which signals to Tiddlywiki that it should be injected into the styles for the page. The actual CSS is in the text of the tiddler, and is small and simple:

.thought {
    border: 1px solid #cccccc;
    padding: 15px;
    margin: 15px;
    box-shadow: 5px 5px 5px #aaaaaa;
}

Of course, the style can be altered (or removed!) to suit your needs.

Rendering Tiddlywiki into HTML

So far, everything we've talked about is really about customizing Tiddlywiki with plugins, styles, and adding some structure to the site using macros and widgets. This is run-of-the-mill intermediate Tiddlywiki stuff. In this section, we'll dive into some tricks to get the wiki rendered as static HTML pages. There are two main hurdles to getting this working:

  1. Scripting tiddlwiki from outside the browser to automate the translation to HTML
  2. Handling links properly

Scripting Tiddlywiki

The build script leverages the tiddlywiki CLI tool fetched via npm with something like npm i tiddlywiki. Critically, Tiddlywiki has 0 dependencies, making it quite robust to dependency issues that are often an issue in the JavaScript ecosystem.

The build script:

  1. deletes the previous build output,
  2. creates a new node-based wiki and import the tiddlers from the appropriate HTML file into it,
  3. uses tiddlywiki to render relevant tiddlers,
  4. renames the entry point to index.html,
  5. uses tiddlywiki to render the CSS, and
  6. opens a browser to view the result.

Here's the code, which should hopefully be clear given the above outline.

#!/usr/bin/env bash

set -euo pipefail

echo -n "Removing old folder..."
rm -rf rpdillon.net
echo "done."

echo -n "Initializing new wiki..."
tiddlywiki rpdillon.net --init server
echo "done."

echo -n "Loading rpdillon.net.html..."
tiddlywiki rpdillon.net --load rpdillon.net.html
echo "done."

echo -n "Rendering pages..."
tiddlywiki rpdillon.net --render '[!is[system]!tag[Draft]![Draft]!tag[Image]]' "[is[tiddler]addsuffix[.html]slugify[]]" text/plain $:/core/templates/static.tiddler.html
echo "done."

# Move the root tiddler to a place the browser knows about.
cp rpdillon.net/output/welcome.html rpdillon.net/output/index.html

echo -n "Rendering css..."
tiddlywiki rpdillon.net --rendertiddler $:/core/templates/static.template.css static.css text/plain
echo "done."

firefox $(pwd)/rpdillon.net/output/index.html

If you're not familiar with Tiddlywiki filter syntax, the filter passed into the render call might be intimidating, so let's take a look. Here it is:

[!is[system]!tag[Draft]![Draft]!tag[Image]]

This filters out (note the ! inverts a filter) system tiddlers (i.e. those that start with $:/), excludes tiddlers tagged with Draft, as well as the Draft tiddler itself, and excludes any tiddler tagged with Image. Images aren't rendered because images are embedded in the associated post rather than viewed singly.

I need to write another post about Tiddlywiki filters, since they really are the core of the logic engine in Tiddlywiki, but for now, all you need to know is that they process input into output, which means they don't only filter information, but also can transform it in other ways. This is critical, because it's how we transform links so they still work in the static version of the site.

Handling Links

One of the challenges when rendering to HTML is how to handle links. In a normal Tiddlywiky, a link to a tiddler called Tech Notes will simply be an anchor link: #Tech%20Notes. But an anchor link in a statically rendered site won't lead to the right place; it'll most likely do nothing. So when rendering tiddlers to HTML, we need to transform them somehow. It wasn't obvious to me initially that this problem has two sides: the tiddlers need to be named in a consistent way when writing the html files to disk, and the links to those tiddlers need to be updated in the same way when rewriting links.

Naming html Files

As I mentioned above, filters can transform data, not just add or remove it. You might have noticed the build script passed two filters to the render command. The first chose what tiddlers to render, but we didn't discuss the second. Here it is again:

[is[tiddler]addsuffix[.html]slugify[]]

There is an additional argument that can be passed to tiddlywiki's render sub-command that tells it how to name the files it outputs. This can put them in a different directory, add prefixes are suffixes, and generally make use of whatever filter operations Tiddlywiki supports. What this filter does is first filters out anything that is not a tiddler, and then to calculate the filename to write the output to, it first adds .html to the filename, and then leverages the built-in filter operation called slugify, which makes the name safe for URLs.

With this transformation in place, now all we need to do is rewrite links to use the same logic.

Rewriting Links

Tiddlywiki is very hackable: all the internal logic that Tiddlywiki uses to operate is exposed and can be used to extend it (this is what I meant with my "Tiddlywiki is a toolkit" comment at the beginning of the post). In particular, Tiddlywiki uses two JavaScript functions to determine how to render paths and links when exporting to HTML. My creating our own version of these JavaScript functions, we can change how Tiddlywiki's behavior when exporting.

I learned how to do this by reading through Part 5 of Didaxy's tutorial on exporting sites using Tiddlywiki. I altered Didaxy's approach to use this.wiki.slugify everywhere, which I think is cleaner and more maintainable. Didaxy's tutorial has 10 parts, and has much larger scope than this write-up. If you're finding that you want to do things not covered here, I highly recommend checking Didaxy's entire tutorial!

get-export-path.js

Rendering of tiddlers is performed by Tiddlywiki's built-in $:/core/modules/commands/rendertiddlers.js. This function checks for the existence of a function called tv-get-export-path, and, if it exists and returns a valid result, uses it to render the paths during export. We can create this JavaScript macro by creating a new tiddler (I used $:/rpdillon/macros/get-export-path.js, but the name doesn't matter), setting a field called module-type to macro and setting the tidder type to application/javascript. Then, the content of the tiddler is our new function:

(function(){
"use strict";

exports.name = "tv-get-export-path";

exports.params = [
    {title: ""}
];

/*
Run the macro
*/
exports.run = function(title) {
    var sanitized_title = this.wiki.slugify(title);
    return ("./"+ sanitized_title).toLocaleLowerCase();
}
})();

Note that when working with JavaScript macros in Tiddlywiki, you'll need to reload the wiki for changes to be reflected properly.

get-export-link.js

In addition to rendertiddlers.js, Tiddlywiki uses its built-in $:/core/modules/widgets/link.js to render the $link widget, which is used throughout the wiki for linking. During export, $:/core/modules/widgets/link.js checks for the existence of a function tv-get-export-link, and, if present, uses it to render links. In the same way we did above, we can create this JavaScript macro. Create a tiddler (I used $:/rpdillon/macros/get-export-link.js), set its module-type to macro, set its tiddler type to application/javascript, and then define the macro to use slugify in the same way we do in the build script and when exporting paths. Here's the code, complete with the comments I left for myself on where I learned the technique from and how I modified it:

// Both get-export-link.js and get-export-path.js 
// are taken from
// https://www.didaxy.com/exporting-static-sites-from-tiddlywiki-part-5
// Those macros are updated here to use 
// this.wiki.slugify, which is built-in
// and produces better output than a simple 
// replace.  This behavior is coordinated with the
// build script, which runs tiddler URLs through
// slugify when exporting them.
(function(){
"use strict";

exports.name = "tv-get-export-link";

exports.params = [];

exports.run = function() {
    var title = this.to;
    var sanitized_title = this.wiki.slugify(title);
    var attr = this.getVariable("tv-subfolder-links");
    var path_to_root="./"	
    var finalLink=path_to_root

    
    var wikiLinkTemplateMacro = this.getVariable("tv-wikilink-template"),
        wikiLinkTemplate = wikiLinkTemplateMacro ? wikiLinkTemplateMacro.trim() : "#$uri_encoded$",
        wikiLinkText = wikiLinkTemplate.replace("$uri_encoded$",encodeURIComponent(sanitized_title));	
    wikiLinkText = wikiLinkText.replace("$uri_doubleencoded$",encodeURIComponent(sanitized_title));
    return (finalLink + wikiLinkText).toLocaleLowerCase();
};

})();

And with this final piece in place, we can proceed to render the wiki statically using the build script.

Future Work

I generally have high confidence that, once set up, using Tiddlywiki to maintain a website is fast, convenient, and easy. There is definitely a hump to get over (as this post illustrates!) if you're not already familiar with Tiddlywiki's workings, though. There are some areas for improvement, however:

  • RSS Feeds - the site structure isn't exactly a "blog", but Thoughts could definitely benefit from an RSS/Atom feed. It might be interesting to export a twtxt-compatible feed as well.
  • Images - I currently embed images into the wiki itself, give them dedicated tiddlers, and then transclude those into whereever I want them. This works fine for a few images that aren't too large, but any image-focused blog will have issues scaling this approach, since it embeds them into theh wiki itself as base64-encoded data URLs.
  • Static hosting of larger files - similarly to the issue with images, the build script has no knowledge of static resources, so the wiki can't link to anything outside itself unless it's hosted separately. This feature requires that I extend Notedeck to support static files, and the build script be aware of that schema so it can copy it to the output folder correctly.
  • Publishing to a server - I haven't discussed how the output gets from your local machine to "production". I use an rsync command, but this could be addressed in the build script, or perhaps by creating a separate deploy script.