This page describes how I set up a pipeline to turn an Obsidian Vault into static HTML and publish it on Netlify, using Quartz and GitHub Actions.
Tangent
Quartz, which does the bulk of the work, is impressive in what it achieves (artful design, some momentum behind it, still a bit Don’t step off the happy path) but I’m having a hard time with its TypeScript nature.
I initially tried to treat Quartz as a shrink-wrapped component in a pipeline - separation of concerns, ease of updates, simplified repository, modularity, encapsulation, yada yada. I tried to limit my customizations to
quartz.config.ts
,quartz.layout.ts
andcustom.scss
.This only works if you’re not trying to do anything complex.
Quartz doesn’t present enough levers to the outside world - even writing a plugin or theming necessitates touching files that should belong to Quartz. I’m now doing something much closer to the official instructions, but I’m not going to lie, it leaves a bad taste.
My approach has the following advantages over the official instructions:
- Build step stays on Github, which has a more generous free tier that Netlify
- Easier to merge in upstream changes
docker compose
means not installing npm on the host machine
But comes with the following caveats:
- My deployment has more moving parts than the official instructions
- There’s no guarantee
npx quartz sync
will work (but I prefer to performgit
actions manually anyway so I didn’t even bother to wire it up) - You’re forced to keep your repo public (if this is a problem, replace “fork” with “clone and push to a new repo” - in fact, I’ll probably update these instructions at some point so that’s the default approach)
If any of these are dealbreakers, official instructions
Overview
We’re going to end up with two branches, my_working_branch
and _publish
. When my_working_branch
changes, a GitHub Action builds the static site and pushes it to _publish
. When _publish
changes, a Netlify deployment pipeline copies public/docroot
to the live site.
We could do this with a single GitHub Action that also pushes the content to Netlify, but installing the Netlify CLI for each build would dramatically increase the wall time of the GH Action. By splitting the work between GitHub and Netlify we speed things up - important when you’re trying to keep within the free tier(s).
Setup
-
Fork
https://github.com/jackyzha0/quartz.git
-
Check it out locally
-
Create your own work branch, based off of the
v4
branch:git checkout v4
git checkout -b my_working_branch
Project layout
.
├── .github
│ └── workflows
│ └── publish.yml
├── .gitignore
├── Dockerfile
├── README.md
├── build.sh
├── content
│ └── index.md
├── docker-compose.ci.yml
├── docker-compose.override.yml
├── docker-compose.yml
├── public
│ └── docroot
├── quartz.config.ts
├── quartz.layout.ts
└── serve.sh
.gitignore
Add the following to the existing .gitignore
workspace.json
describes the Obsidian application’s local state, and will cause you no end of grief if you try to keep it sync’d across multiple Obsidian instances. Better to just ignore it.
Docker configuration
Dockerfile
We need to copy in package*.json so we can run npm install
when we set up the container - mounting directories happens after the container’s built.
8080
is where the rendered content is hosted when you --serve
the site. 3001
is for a websocket callback that hot-reloads the browser preview. The ports are exposed here, but are bound to the host system later on, in docker-compose.yml
. Neither port is necessary for building the site, only --serve
ing it.
docker-compose.yml
This is run for all docker compose
commands. It maps our project into the Docker image, but carves out an exception for the node_modules
that we installed with npm install
when we set up the container.
docker-compose.override.yml
This only runs when we’re --serve
ing the content. Maps ports 3001 and 8080 to the outside world, which is not necessary when we’re just building.
Quartz’s output directory can’t be a Docker volume mount, because Quartz tries to rmdir
it. We’re not doing that currently, but I still prefer to go one level deeper, just in case I ever need to mount public/
.
docker-compose.ci.yml
This only runs when we’re building the content.
Shell scripts for controlling Docker
These are just for convenience. The -d
detaches from the terminal so the container runs in the background. The --build --force-recreate
is because I really don’t trust Docker as far as I can fling it.
Don’t forget to chmod +x
these scripts if you use them.
serve.sh
build.sh
Obsidian setup
content/index.md
---
title: Welcome
---
Hello World
Once you’ve created that file, create a new Obsidian Vault in the content
directory, via the “Open folder as vault” choice. Quartz starts building your site from index.md
, and it’s just easier to create it manually.
You can now edit the Quartz config files as you see fit, and build and preview the site locally. You should be able to run ./serve.sh
and see your site at http://localhost:8080/
GitHub Action configuration
This Action fires when my_working_branch
changes. It performs the site build, and pushes the result to the _publish
branch.
Note
For the avoidance of confusion, the
v4
in this script refers to the version of the github checkout action we’re using, not the branch in the Quartz repo.
.github/workflows/publish.yml
Netlify configuration
On the Netlify management console, Add new site->Import an existing project->Deploy with GitHub
.
Set the following properties:
Base directory: /
Publish directory: public/docroot
Production branch: _publish
That’s about it. Any Obsidian content you push to my_working_branch
should turn up on your live site about three minutes later.
Tracking upstream
This is how you keep up-to-date with new versions of the Quartz application.
As a one-time thing, set a remote that points to the official Quartz repo. By convention, we call your repo origin
and the remote repo upstream
:
git remote add upstream https://github.com/jackyzha0/quartz.git
Now, when you want to grab the latest changes from the official repo, you do this:
git checkout v4
git pull upstream v4
At this point you can either merge the upstream changes into your working branch, or rebase your working branch on top of the changes. I prefer rebase, but merge is considered simpler:
git checkout my_working_branch
git merge v4
There’s a chance you’ll get merge conflicts. Unfortunately I can’t help unpick that, but there’s a lot of helpful material out there.
After a merge, make sure you test the site locally (./serve.sh
) before publishing it.
Note
If you add an Obsidian plugin that automatically pushes to git (eg obsidian-git) into the mix, you effectively get continuous deployment of your Obsidian vault - with the caveat that in my experience obsidian-git struggles with merge conflicts if you use it on more than one machine simultaneously.
Idle thoughts: would unison or dropbox do a better job? Is it possible to publish from dropbox? Would it be possible to add CRDT-like functionality to Obsidian as a plugin? (Hah. Somebody announced one 13 hours ago as I’m editing this).