Critiques might be outdated

My analysis and criticisms below only apply to the version of Quartz I used. That was the version that existed on or around August 28, 2023.

I’m certain updates have been made that might make these moot, and I’ll probably go and pull them into my version at some point.

My version is so heavily modified at this point, upstreaming any changes would be difficult, since it’s diverged quite a bit.

So, after trying to write a static site generator and getting lost in the weeds of writing a Markdown parser (for the fun of it), I ran into enough roadblocks that made me go looking for other options for doing static site generation from an Obsidian vault.

If you don’t know, Obsidian is a personal knowledge base backed by their own flavor of Markdown with extra features, and it has one of the best Markdown editors I’ve used. Generating a site off of it using Obsidian HTML I was bumping into limitations.

Enter Quartz. Quartz looks like it’s trying to match what Obsidian’s own Publish service can provide. Some of the built-in features include Full-Text Search (completely client side), a Graph View for navigating non-hierarchically, KaTeX\KaTeX for math rendering, as well as other features.

First, some pain points

These weren’t show stoppers as I was able to work around them and in a few cases, I’ll probably look at pushing them upstream. *I do have a lot of complaints here, but let me be clear, I’m very happy with what Quartz gives me, and I’ll explain why later.

By default Quartz creates <a href> tags for wiki-style links to pages that don’t exist in the published vault. I would rather these links be given a class so that they can be styled differently and either go to a shared “non-existent page”, or not link to anything at all.

I was able to change the builtin Links plugin to change its behaviour when it encounters wiki-style links for pages that don’t exist. Now I can make them greyed out and provide a helpful message.

Lots and lots of inline JavaScript

The way the JavaScript code is put together means that each page has a copy of the code in a <script> tag at the bottom of the page. This is very wasteful, especially given that it’s not even minified. More recent versions of Quartz do some minifying using ESBuild but the code could still be extracted into a separate file that the browser could cache instead of it being duplicated on each page.

With minifying, each page is still about 4 MiB each, which is just way too much. I wonder how much of this is just because the authors aren’t thinking about low-bandwidth environments. I want to make my site as accessible as I can.

Google Fonts

I had to do some edits to remove the imports for Google Fonts, since I want to minimize any third party requests. *There’s still some calls out to CDNs for some JavaScript libraries, like KaTeX\KaTeX, and I’d like to bring that in and make it self-hosted if I can.

It looks like you can just set fontOrigin to "local" in the ComponentResources plugin and it will prevent it from loading the Google Fonts JavaScript, but this still leaves both of the <link rel="preconnect" .../> tags in the <head> of the page, which is still potentially connecting the client to Google’s infrastructure.

Obsidian’s default hard breaks

By default, Obsidian treats line breaks as hard breaks in a lot of places. This means there is some syntax that Obsidian will accept that a regular Markdown parser will render differently. *Markdown does specify a way to insert hard breaks, but it uses significant end of line white space.face with raised eyebrow

Obsidian has an option to use strict line breaks to make it match the regular Markdown behaviour, but I’ve grown used to it as it is. So I modified the ObsidianFlavoredMarkdown plugin to add a configuration option for strict hard breaks and when it’s set to false it will add the remark-breaks plugin in so that the default Obsidian behaviour will work.1

Path endings

For some reason that’s unclear to me, Quartz generates HTML files for each piece of content but when it renders links, it doesn’t put the .html file extension on it.

This breaks static site hosting using Amazon S3, and maybe others. They don’t seem to address this in the documentation, except in reference to deploying to Vercel, which isn’t what I’m using.

I had to hack up the link generation to add back the extension where it was needed and then repeat the process in the graph, search, and backlinks code, since it’s not shared. I’d like to be able to make a cleaner fix for this and add a configuration option, but I’ll need to take some time to do that.

Images wrapped in paragraphs

Individual image tags end up wrapped in <p> tags by default, and that makes it difficult to style them. I added a configuration option, unwrapImages, to the ObsidianFlavoredMarkdown plugin, that adds the remark-unwrap-images plugin.

Default theme configuration options are limiting

I feel like the number of color options in the theme configuration settings is limiting, and also confusingly named. For example, "light" is used in both the dark and light theme for the background color, but obviously in the dark theme it’s not going to be “light”. Also because there are so few colors defined, you end up with things like the highlight for lines in a code block uses the same color as <strong> tags, which usually have different needs.

CSS styling confusion

Styles are defined in at least three places, the top level CSS files, inline CSS of the components themselves, and then some imported CSS in the components, making it a bit harder to track down what’s changing what.

I’ve also found the custom.sass file to be almost useless. Because it’s included in base.sass before the rest of that file, it’s hard to get styles in the custom file to override ones in the base file. It would be better for me to just merge my styles into the base.sass file to get a cleaner configuration, or maybe break things up by what part of the page they’re for.

I’m probably going to just end up rewriting the color options to be more functional, or maybe follow the base16 approach.

Emoji pains

This one isn’t specific to Quartz. They just happen to provide an easy way to solve it.

I decided to use a few emoji for things on my site and quickly discovered that it doesn’t render consistently on things like Windows.

To fix this, I wrote a Rehype plugin that uses OpenMoji SVG images to replace the vast majority of emoji I’m using on this site. And I’ve given it configuration options for downloading them so I can self-host only the ones I use. So that means I can put things like party popperpartying face and get decently consistent results. They don’t work as well on a dark background though.

It’s another thing I’d like to polish up and publish for others to be able to use.

And now to the good stuff

With that out of the way, let’s talk about some features of Quartz that I really like and mostly just come out of the box. There are the features I mentioned earlier, but I’ll go into more detail on one of them.

The built in syntax highlighting is using Rehype Pretty Code which wraps the shiki syntax highlighting library. And I have to say, it’s very nice. *The shiki library is another wonderful piece of work. Having done a lot of recent research into syntax highlighting options, this one really stands out.

Some of the great features include:

  • It uses VS Code themes and TextMate grammars.
  • It comes with a good set of languages built in.
  • It has a “language” for displaying content with ANSI escape sequences. This is very cool and allows for displaying shell sessions and other terminal content without sacrificing the colors that you’d see in the terminal.
  • For code blocks:
    • Basic language selection using GitHub Flavored Markdown syntax.
    • Optional line numbers (they’re on by default in Quartz so I had to modify the plug-in that wraps it so I could have the option, another thing that I might make into a configurable option and submit upstream).
    • Single and multiple line highlighting
    • Highlighting of words by pattern
  • For inline code snippets: *The inline code highlighting is a nice touch and really adds to the presentation of code explanations.
    • Options to specify language or token types to highlight inline code, e.g. a generic keyword, function_name and string; a Python function declaration line def bar():.2
Example using 'showLineNumbers {2-3,5} /foo/'
def foo():
	print "bar"
	print "baz"
 
# Comments are a good idea
def bar():
	print "quuz"
	print "foo"

I especially like the ANSI escape highlighting. It makes showing shell output much more enjoyable.

Example of ANSI escaped content copied from my shell
Detected change, rebuilding...
Parsed 1 Markdown files in 65ms
Filtered out 1 files in 75μs
Emitted 127 files to `public` in 336ms
Done rebuilding in 412ms
[200] /garden/quartz-and-obsidian-for-static-sites
[200] /index.css
[200] /prescript.js
[200] /postscript.js
[200] /static/contentIndex.json
[200] /static/data/color/svg/1F928.svg
[200] /static/data/color/svg/1F389.svg
[200] /static/data/color/svg/1F973.svg
[200] /static/data/color/svg/21A9.svg
[200] /static/external.svg

Easy extensibility

Quartz has a plugin system that builds on top of Remark and Rehype, *I have been exceedingly pleased by the Remark and Rehype libraries. They’ve made features I’ve been wanting to add so easy to implement. two libraries I was unfamiliar with, and provides an interface for easily wrapping plugins from those ecosystems. This means, for example, that instead of having to come up with an ad-hoc approach for extending Markdown, I can use something like remark-directive to easily add custom “directives”3.

For example, I wanted an easy way to indicate that a section should use multiple columns. So I added a block directive :::columns{count=N}.

That directive wraps the content in a
<div style="columns: N"> and it just works, as you can see with this paragraph and the next (if the window is wide enough).

I’ve also added :::asides like the one to the right (or inline if the window is too narrow). I’m still trying to figure out where I prefer those vs. footnotes4.

These asides are clever...

…if I say so myselfwinking face. The :::aside block directive and the :aside text directive works on wrapped content, and that content supports regular Markdown. Which is why the one to the right “Why not just HTML” *Why not just use HTML? Obsidian doesn’t support Markdown inside of HTML tags5, so that wasn’t an option. Plus it seems a bit heavy next to Markdown syntax. can have a footnote link in it.

Raw Markdown for the aside mentioned
[start of paragraph] ... :aside[**Why not just use HTML?** Obsidian doesn’t support Markdown inside of HTML tags[^quartz-html], so that wasn’t an option. Plus it seems a bit heavy next to Markdown syntax.] ... [rest of paragraph]
 
[^quartz-html]: Quartz **does** support rendering Markdown inside of HTML through a configuration option, `enableInHtmlEmbed{:.variable}`, but since Obsidian doesn't, it makes it hard to use.

Normally that content would appear inline, but I can use float to move it to the right. On top of that the vertical positioning is adjustable using an offset parameter that controls a CSS variable, --aside-offset, in the inline style of the <span class="aside-content"> *I’d love to use an <aside> tag here, but that breaks the surrounding paragraph and means I can’t use it in the middle of a paragraph like I am here. See “Tag omission” on the MDN page for the <p> tag element which I can then access in my main CSS file and use to control the offset however I need to, like this:

.aside-content {
    /* Here I'm using the whole variable for the value, meaning I pass in
     * the unit along with the number, but I could do whatever I want
     * since it's just a variable, and I control both sides of it. */
	top: var(--aside-offset);
	position: absolute;
	/* The rest of the styling... */
}

But that alone would be difficult to use by itself because it would be relative to the top of the page.

To fix that, I’m wrapping the content <span> in a <span class="aside-wrapper">. I can make that have .aside-wrapper { position: relative; float: right; clear: right; } and use the offset of the :::aside directive to control the positioning relative to its original location, e.g. :::aside{offset=-2em} moves it 2 em up from where it was originally.

On mobile displays, the asides are inlined, and in the case of ones in the middle of a paragraph, we break the text by inserting ellipses into the flow.

I’ve already spoken about the extensibility through the plug-in system, which makes adding features fairly easy. Remark and Rehype really help with that, as they seem like good libraries for handling Markdown and HTML, respectively. And they already have a large existing list of plugins, both Remark plugins and Rehype plugins, that are easy to slot into Quartz6.

Overall, I’m very pleased with the results and feel like I can focus on writing again instead of fighting with my tooling.

Footnotes

  1. There was a recent PR to Quartz that adds a plugin to handle this. I feel like this belongs in the ObsidianFlavoredMarkdown plugin and not something separate, given how tied it is to Obsidian’s default behaviour, but that’s something for the maintainers to decide.

  2. There’s a section of the Retype Pretty Code documentation that talks about “context-aware inline code”, which made me think the library was smart enough to look at neighboring code blocks and determining the token types for inline code snippets from that, but it doesn’t seem to be the case. Maybe I’m missing something?

  3. Based off of the syntax proposed for generic directives (archive link since their site is currently unresponsive).

  4. When reading books I prefer footnotes over endnotes from a usability perspective. Half the time when I look up an endnote and it turns out to just be a citation note, I’m a little annoyed at the author.

  5. Quartz does support rendering Markdown inside of HTML through a configuration option, enableInHtmlEmbed, but since Obsidian doesn’t, it makes it hard to use.

  6. Some of my modifications required deep changes in Quartz’s components. I’m not sure how to make those more modular so I could share them. An example of this is the way the :::aside directives mark the file as “having asides”, so that later during page generation, I can add a has-asides class to one of the container elements, so that I can remove the right margin when it’s not needed. This can be seen by comparing this page’s layout to the home page’s layout.