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.
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.
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!
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
.
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.
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.
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.
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.
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.
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:
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:
index.html
,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.
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.
html
FilesAs 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.
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.
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:
data
URLs.