The Quartz 5 site looked straightforward until I started treating it like a real published system instead of a pleasant little garden experiment. That is usually when the interesting parts show up: the boundary between authored content and infrastructure, the comment system integration, the analytics wiring, and the question of how to keep upgrades possible without turning the whole thing into a bespoke fork.

This is the cleaned-up version of the implementation work behind mind.ruhanirabin.com. I am keeping the examples public-safe, so all hostnames, paths, and site values below are sample values only. This digital garden is hosted with Cloudflare Pages.

What I was trying to build

The goal was not just to “run Quartz” and get it done with. The goal was to run a maintainable Quartz 5 implementation for a public digital garden while keeping a clean boundary between:

  • Authored content owned by the vault workflow
  • Infrastructure-owned Quartz configuration
  • Deployed site behavior
  • Comments and analytics integrations
  • Future upgrades without painful merge shenanigans

🧱 That boundary matters. If you let everything blur together, the next Quartz update becomes a rabbit hole I won’t be able to dig myself out from.

The repository and working model

The setup used a dedicated infrastructure worktree alongside the authoring vault.

Repository: https://github.com/example/digital-garden-quartz
Infrastructure worktree: /home/example/git-projects/digital-garden-quartz-infra
Authoring vault: ~/git-projects/digital-garden-quartz
Integration branch: v5
Production site: https://mind.example.com/
Hosting: Cloudflare Pages

The important part is not the exact names. It is the ownership split.

Ownership boundary

  • The authoring workflow owns published content/**, templates/**, and .obsidian/**
  • Quartz Syncer publishes content directly from Obsidian using a Git token to the integration branch as its bot identity
  • Infrastructure branches own Quartz configuration, quartz/**, package files, deployment files, repository docs, and static assets outside the vault-owned paths
  • Infrastructure changes start from the latest integration branch and move through pull requests
  • npx quartz sync is not part of this workflow
  • content repo and infrastructure repo are a checkout of the same head but work on different purposes. Obsidian uses one for content; I use the other one for infra related changes.

That last line saved me from future confusion. I wanted the content workflow and the infrastructure workflow to stay separate, because once they merge too early, nobody remembers which change came from where.

The site-level changes

The site itself was not vanilla Quartz anymore. It had a few local modifications that were worth keeping as first-class implementation details.

SPA navigation stayed enabled

Quartz 5 renders the site with SPA navigation enabled. That was important because the comments system and some of the local components needed to survive route changes without losing their state every time a link was clicked.

I added a site-local footer so external links behaved safely and still used noopener noreferrer.

That sounds small. It is. But small changes like that are the kind people notice only when they are missing.

Analytics and comments were self-hosted

The site used self-hosted services instead of default third-party dependence:

  • Umami analytics self-hosted at https://um.example.net
  • Remark42 comments self-hosted at https://remark.example.net
  • site namespace digital-mind

Again, those URLs are sample values here. The real point is that both services were wired into the Quartz deployment as local infrastructure choices rather than loose add-ons.

Add Remark42 without forking Quartz core

The comments integration was the part I wanted to keep upgradeable.

remark42 self hosted integrated quartz 5

Rather than forking Quartz core, I registered the local Remark42 component under Quartz’s existing Comments component key and placed it in the normal afterBody slot. That kept page-type layout resolution inside the supported structure instead of making every future update a merge conflict with opinions. This took a while, due to the reason that there was no direct documentation on integrating it with Quartz 5. There was a plugin in GitHub that was more than two years old. So, I did not pick that up but went along with my own agnostic solution.

A couple of details that are important here:

  • the shared frontend config set no_footer: true
  • the site hid Remark42’s signature/footer on initial load and on later SPA navigation
  • pages could opt out of comments with comments: false in frontmatter

That gave me a working default while still allowing page-level control.

The homepage override

The homepage got a local Recent Notes component override. It rendered two build-time dynamic islands after the authored Markdown and before comments:

  • five recently updated garden pages
  • five recent fixes

The implementation did a few practical things:

  • marked only the first item in each list as Latest
  • preserved internal-link popovers
  • stacked from two columns to one at the existing 800px mobile breakpoint

That kept the homepage useful without making it look like it had wandered out of a dashboard theme shop.

quartz-5-mind-ruhanirabin-com

Remark42 deployment details

Remark42 ran in Docker Compose under EasyPanel with EasyPanel-managed SSL.

The core backend settings were:

SITE=digital-mind
ALLOWED_HOSTS='self',https://mind.example.com
AUTH_SAME_SITE=none

The frontend and backend site_id values had to match. That is not one of those settings you casually improvise later.

If you want to share the same Remark42 instance with another site, the pattern is the same in principle:

  • add another SITE identifier
  • add the corresponding origin to ALLOWED_HOSTS
  • configure the matching frontend identifier

The practical warning is also the obvious one: comment data, secrets, administrator credentials, and backups do not belong in the public repository.

Email and administration came later

Email authentication, reply subscriptions, and per-comment admin notifications were all separate operational concerns, even though they lived in the same system.

The later runbook for that setup covers the full SMTP and moderator workflow. This prequel only needs the structural part:

  • anonymous access stayed enabled
  • email features were planned as independent toggles
  • moderator access would use the exact email-authenticated Remark42 user ID
  • SMTP credentials and signing secrets stayed in EasyPanel, not in the public repo

What changed over time

The implementation evolved through a series of focused pull requests rather than one giant rewrite.

  • added the local Remark42 component, scoped styling, SPA cleanup, and theme synchronization
  • integrated the component through Quartz’s YAML-resolved Comments layout slot
  • defined window.remark_config before loading the embed script and handled the first instance cleanly
  • replaced the child <noscript> element with plain loading text so Remark42 would create an iframe correctly
  • documented the durable SilverBullet note path and automation handoff rules
  • treated generated SilverBullet service and inventory pages as read-only output
  • enabled the documented no_footer option in the shared SPA-aware configuration
  • added the homepage-only recent-content islands
  • refined dark-mode contrast and interaction feedback without changing light-mode resting colors

That list sounds neat now. It felt less neat while I was doing it.

Verification that mattered

The useful checks were the boring ones:

  • TypeScript and Prettier checks passed for the changed code
  • Quartz builds completed successfully
  • the generated output included the Remark42 container on the rendered pages
  • live browser checks confirmed the comment iframe loaded correctly
  • live requests for the iframe document, configuration, authentication status, and comment lookup returned HTTP 200
  • the final deployment rendered a visible Remark42 iframe on the production site

The point of the verification was not to admire the build system. It was to make sure the site still worked after the pieces were separated.

Why the boundary mattered

This part turned out to matter more than I expected.

By keeping the Quartz site changes inside infrastructure-owned files, I could keep the authoring workflow and the site runtime from stepping on each other. That made future upgrades less risky and kept the content system from becoming a quiet hostage situation.

It also made the later Remark42 email/admin work much easier to reason about, because the comment system was already integrated as a site capability rather than a one-off hack.

What I would keep doing

If I were building this again, I would keep the same general rules:

  • separate vault-owned content from infrastructure-owned code
  • keep comments as a local component, not a core fork
  • keep analytics and comments self-hosted where practical
  • preserve the upgrade path
  • treat sample values as sample values in public notes

The short version is that Quartz can stay pleasantly flexible if you resist the urge to glue everything together with hope.

Is that the end? No, it is only the beginning.

This was a documentation of my 9 day journey with it, many more to come.