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.

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, 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.

By default Quartz creates <a href><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.

Google Fonts

I had to do some edits to remove the imports for Google Fonts, since I want to minimize any third party requests.

It looks like you can just set fontOriginfontOrigin to "local""local" in the ComponentResourcesComponentResources plugin and it will prevent it from loading the Google Fonts JavaScript, but this still leaves both of the <link rel="preconnect" .../><link rel="preconnect" .../> tags in the <head><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.

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 ObsidianFlavoredMarkdownObsidianFlavoredMarkdown plugin to add a configuration option for strict hard breaks and when it’s set to falsefalse 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.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><p> tags by default, and that makes it difficult to style them. I added a configuration option, unwrapImagesunwrapImages, to the ObsidianFlavoredMarkdownObsidianFlavoredMarkdown 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><strong> tags, which usually have different needs.

I’ve also found the custom.sasscustom.sass to be almost useless. Because it’s included in base.sassbase.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.sassbase.sass file to get a cleaner configuration, or maybe break things up by what part of the page they’re for. There are also styles defined in the component’s JavaScript which is another source of confusion.

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.

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:
    • Options to specify language or token types to highlight inline code, e.g. a generic keywordkeyword, function_namefunction_name and stringstring; a Python function declaration line def bar():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"
Example using 'showLineNumbers {2-3,5} /foo/'
def foo():
	print "bar"
	print "baz"
 
# Comments are a good idea
def bar():
	print "quuz"
	print "foo"
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
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, 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}:::columns{count=N}.

That directive wraps the content in a
<div style="columns: N"><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:::aside block directive works on wrapped content, and that content supports regular Markdown. Which is why the one to the right “Why not just HTML” can have a footnote link in it (including the footnote definition in a separate footnote reference block).

Raw Markdown for the aside mentioned
:::aside{offset=-6em}
**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.
 
[^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.
:::
Raw Markdown for the aside mentioned
:::aside{offset=-6em}
**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.
 
[^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 absolute positioning to move it to the right. On top of that the vertical positioning is adjustable using an offsetoffset parameter that controls a CSS variable, --aside-offset--aside-offset, in the inline style of the <aside><aside> element which I can then access in my main CSS file and use to control the offset however I need to, like this:

aside { 
    /* 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... */
}
aside { 
    /* 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 <aside><aside> in a <div class="aside-wrapper"><div class="aside-wrapper">. I can make that have .aside-wrapper { position: relative; }.aside-wrapper { position: relative; } and use the offsetoffset of the :::aside:::aside directive to control the positioning relative to its original location, e.g. :::aside{offset=-2em}:::aside{offset=-2em} moves it 2 em up from where it was originally.

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 ObsidianFlavoredMarkdownObsidianFlavoredMarkdown 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, enableInHtmlEmbedenableInHtmlEmbed, 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.