Why I’m building a separate notes site
Phase 1 is done: a Quartz 5 site now builds and runs on my Debian desktop, ready to become notes.ny2honolulu.com. This post documents every step, including the ones I got wrong.
The idea came from No Boilerplate’s “Obsidian for Learning” series. The method sorts notes into four kinds: fleeting notes (my own quick thoughts), literature notes (other people’s ideas from books, lectures and videos), atomic notes (one idea per note, in my own words, linked to others) and project notes. I’m applying it to Black Hat Python and the AWS DVA-C02 study guide. Credit for the method goes to No Boilerplate; I’m not republishing his vault, only my own notes built with the approach.
I wanted a public home for the best of those notes, a “digital garden,” without stuffing them into WordPress. Quartz turns a folder of Markdown notes into a static website, and it understands Obsidian’s [[wikilinks]], so links between notes keep working.
The project follows my planning → build → results pattern, split into three phases:
- Phase 1 — Local build (this post): repo, install, security triage, first preview.
- Phase 2 — Privacy and personalization: turn on opt-in publishing, customize the site.
- Phase 3 — Deploy: GitHub Pages plus a DNS record for the subdomain.
Security decisions made before writing a single command
The biggest risk in this project is leaking private notes, so I settled the privacy design first.
A static site keeps WordPress out of it. Quartz output is plain HTML, CSS and JavaScript: no PHP, no database, no login page. The notes site adds no new attack surface to my WordPress install, which I recently had to patch against an actively exploited SQL injection.
The repo is public, so everything committed is public. Free GitHub Pages sites are served from public repositories. A publishing filter only controls what appears on the website; anything in the repo is still readable on GitHub. Pointing Quartz at my whole Obsidian vault would expose my lab notes through the repo, even if the website hid them.
So I use two layers (defense in depth):
- Only public notes go in the Quartz folder. My study vault stays completely outside the repo. During setup I chose “Empty Quartz,” not “Copy” or “Symlink” an existing folder.
- Opt-in publishing as a second check. In Phase 2 I’ll enable Quartz’s ExplicitPublish plugin, so a note appears only if it carries
publish: truein its frontmatter. If I forget the flag, the note stays private. That’s secure by default.
And the post itself is sanitized. Terminal prompts below are trimmed to $. I’ve left out my username, home paths and the other services running on my machine, because a public post is recon material too.
Build log: from zero to a cloned repo
Getting the repo onto my machine took longer than it should have, and the mistakes taught more than the successes.
Step 1: Check Node.js
Quartz 5 needs Node v22 or newer and npm v10.9.2 or newer.
$ node -v
v22.23.2
$ npm -v
10.9.8
Both pass. Next I checked where Node actually comes from, since my prompt showed an active conda environment:
$ which node npm
~/.local/share/mise/shims/node
~/.local/share/mise/shims/npm
Node is managed by mise, a tool version manager. A “shim” is a small stand-in program that picks the right Node version for the folder you’re in. Good news: switching conda environments won’t break Quartz.
Mistake 1: checking the version in the wrong folder
mise can select a different Node version per project, so the check only counts inside the project folder. My first mise current node ran in my home directory, which proved nothing. Lesson: read the prompt before trusting the output.
Mistake 2: cloning a repo that didn’t exist
$ git clone https://github.com/ny2honolulu/notes.git ~/projects/notes
remote: Repository not found.
fatal: repository 'https://github.com/ny2honolulu/notes.git/' not found
bash: cd: ~/projects/notes: No such file or directory
npm error enoent Could not read package.json
I hadn’t created the repo yet. One failure caused three errors because I pasted several commands at once, and bash runs every line even after one fails. The npm error happened because cd failed and npm ran in my home folder.
The fix is &&, which means “run the next command only if this one succeeded”:
$ git clone https://github.com/ny2honolulu/notes.git ~/projects/notes && cd ~/projects/notes && npm i
A side lesson: GitHub returns “not found” for private repos too, rather than “access denied.” That prevents attackers from enumerating which private repos exist.
Mistake 3: pasting a URL into the terminal
Bash tried to run https://github.com/jackyzha0/quartz as a program. Web addresses go in the browser, or through xdg-open <url>, which opens the default browser from the terminal.
Step 2: Create the repo from the template
On the Quartz GitHub page I clicked Use this template → Create a new repository, named it notes, set it to Public, and left “Include all branches” unchecked. The template has 25 branches, but I only need the default one, v5.
A template gives a clean repo that’s entirely mine, without a fork’s link back to the original project.
Step 3: Verify, then clone
$ git ls-remote https://github.com/ny2honolulu/notes.git
a50fb44... HEAD
90a4122... refs/heads/dependabot/github_actions/ci-dependencies-...
565aa1e... refs/heads/dependabot/npm_and_yarn/production-dependencies-...
f3aac8b... refs/heads/dependabot/npm_and_yarn/typescript-...
a50fb44... refs/heads/v5
ls-remote lists a repo’s branches without downloading anything. The surprise: Dependabot had already opened three pull requests on my brand-new repo. The template includes a .github/dependabot.yml file, so GitHub’s update bot started work immediately. I’m leaving those PRs unmerged until I have a working baseline.
Note the branch name: v5, not main. That will matter for the deploy in Phase 3.
$ git clone https://github.com/ny2honolulu/notes.git ~/projects/notes
Receiving objects: 100% (345/345), 12.64 MiB | 4.33 MiB/s, done.
$ cd ~/projects/notes
$ mise current node
22.23.2
The repo’s .node-version file asks for Node 22, and mise confirms 22.23.2 inside the project.
Step 4: Install dependencies
$ npm i
added 328 packages, and audited 329 packages in 24s
6 high severity vulnerabilities
npm i reads package.json and the exact pinned versions in package-lock.json, then downloads everything into node_modules/. The six “high” findings needed a closer look before going further.
Triage: six “high” vulnerabilities, two fixed, one accepted
npm audit showed only three actual problems. The count was six because one bug was inherited by three other packages.
| Package | What it does in Quartz | The bug | My exposure | Outcome |
|---|---|---|---|---|
sharp (below 0.35.5) | Image processing for page previews, using librsvg for SVGs | CVE-2026-96889: a flaw in librsvg reachable through a malicious SVG | Real. I make my own SVGs and might add SVGs from the web | Fixed by npm audit fix |
source-map-js (1.0.0–1.2.1) | Maps compiled JavaScript back to its source during the build | A crafted source map can freeze the event loop (denial of service) | Very low: it only reads Quartz’s own files | Fixed by npm audit fix |
braces → micromatch → fast-glob → globby | Finds files with patterns like content/**/*.md | CVE-2026-93687: deeply nested brace patterns exhaust the stack and crash Node | Very low: patterns come from Quartz’s code and my config | No fix exists. Risk accepted |
The key question for each finding was not “how high is the score?” but “can an attacker actually reach this in my setup?” Quartz runs on my desktop, builds the site and exits. The published website is static files that contain none of this code.
Applying the safe fix
All three said “fix available via npm audit fix,” without --force. That matters:
npm audit fixupgrades only within the version rangespackage.jsonallows. Low risk of breaking anything.npm audit fix --forcecan jump to major new versions past what Quartz was tested with. It often breaks builds.
$ npm audit fix
changed 7 packages, and audited 327 packages in 11s
4 high severity vulnerabilities
The count dropped from 6 to 4. sharp and source-map-js were fixed. The braces chain remained, and npm still claimed a fix was available.
Why npm’s “fix available” was wrong
The audit listed braces with a version range of *, meaning “every version.” That contradicted the “fix available” line, so I checked the advisory (GHSA-vfj7-8cjw-p6xm). It’s marked Unreviewed, with both affected and patched versions listed as Unknown. GitHub’s own page says Dependabot alerts aren’t supported for it, because there’s no package with an affected and fixed version.
So npm flags every version, and no fixed version exists to upgrade to. Running npm audit fix again would loop forever.
Why a CVSS 8.7 doesn’t scare me here
The score assumes the attack vector is Network with no privileges required. That fits a web service accepting patterns from strangers. It doesn’t fit a build tool on my desktop that reads its own patterns. The worst realistic case is that my own build crashes.
Decision: risk acceptance, documented here. When braces ships a patched version, npm audit or Dependabot will surface it.
$ git status
On branch v5
Changes not staged for commit:
modified: package-lock.json
The fix lives in package-lock.json. I’m not committing it until the site builds successfully with the new versions; if something breaks, git restore package-lock.json returns me to the tested baseline.
Setting up the site and the first preview
The site rendered at localhost:8081 on the first try, after I sidestepped a port conflict.
Step 5: Run the setup wizard
$ npx quartz create
┌ Quartz v5.0.0
◇ Choose a template for your Quartz configuration
│ Obsidian
◇ Choose how to initialize the content in `.../notes/content`
│ Empty Quartz
◇ Enter the base URL for your Quartz site (e.g. mysite.github.io/quartz)
│ notes.ny2honolulu.com
Created quartz.config.yaml from 'obsidian' template
✓ All configured plugins are already installed
| Question | My answer | Why |
|---|---|---|
| Template | Obsidian | Understands [[wikilinks]], callouts and embeds. Other choices were Default, TTRPG and Blog |
| Content | Empty Quartz | Layer one of the privacy plan. “Symlink an existing folder” would push every private note to public GitHub |
| Base URL | notes.ny2honolulu.com | Quartz builds every link from it, so it must match where the site will live. No https://, no trailing slash |
Step 6: Avoid a port conflict
Quartz previews on port 8080 by default, but another local service already listens on 8080. Two programs can’t share a port, so I moved Quartz to 8081:
$ npx quartz build --serve --port 8081
Quartz v5.0.0
⚠ Could not load plugin "@quartz-community/obsidian-plugin-excalidraw" to detect category. Skipping.
Found 1 input files from `content` in 25ms
Warning: content/index.md isn't yet tracked by git, dates will be inaccurate
Parsed 1 Markdown files in 101ms
Filtered out 0 files in 50μs
[QuartzTheme] Theme package "@quartz-themes/default" is not installed. Installing...
added 3 packages, and audited 330 packages in 8s
4 high severity vulnerabilities
Emitted 48 files to `public` in 16s
Started a Quartz server listening at http://localhost:8081
One starter note produced 48 files, because Quartz also builds the search index, graph data, styles and scripts. The command keeps running as a server until stopped with Ctrl + C.
Three warnings worth recording
- Excalidraw plugin couldn’t load. The Obsidian template lists it, yet the wizard had just reported every plugin installed. That doesn’t add up. It’s not fatal, and I’ll decide in Phase 2 whether to install it or turn it off.
- The theme installed itself mid-build. Quartz ran an npm install on its own and added three packages. Convenient, but it’s code downloaded without my asking. The audit still showed the same 4 braces findings, so nothing new came in.
- Dates will be inaccurate. Quartz reads page dates from git history, and the starter page isn’t committed yet. That resolves after the first commit.
The result
The browser showed the blank Quartz 5 site: a title, search box, light/dark toggle, reader mode, an empty Explorer, the “Welcome to Quartz” starter page dated Oct 07, 2026, and a Graph View with a single dot. One dot means one note; it grows as I link notes with [[ ]].
Lessons learned and what’s next
The most useful lesson from Phase 1: a security tool’s output is a starting point for judgment, not a verdict.
- Triage beats severity scores. “High” describes the worst case for any program using a library. My exposure decided which fix mattered (sharp) and which risk I could accept (braces).
- Tools can contradict themselves. npm said “fix available” for a bug with no patched version. Reading the advisory resolved it.
- Chain commands with
&&. Bash keeps going after a failure unless told not to. - Check context, not just output. A correct version number from the wrong folder proves nothing.
- Privacy is decided before the first commit. In a public repo, everything committed is public, regardless of what the website shows.
Connections to what I’m studying:
- AWS DVA-C02: Quartz on GitHub Pages is the same pattern as S3 static website hosting behind CloudFront. The Phase 3 DNS step mirrors pointing Route 53 at a distribution. Keeping secrets out of the repo is the same rule as never committing AWS access keys.
- Black Hat Python: the malicious-SVG risk in sharp is the classic harmless-looking file aimed at a parser. GitHub’s “not found” for private repos is an anti-enumeration defense, the flip side of the book’s recon techniques. Template plugins that load code automatically resemble the book’s trojan that pulls modules from a repo.
Phase 2 — Privacy and personalization:
- Review
quartz.config.yamland enable ExplicitPublish - Decide on the Excalidraw plugin
- Set the site title and footer links
- Write a real homepage with
publish: true - Confirm the build still passes, then commit
package-lock.jsonand the config
Phase 3 — Deploy: GitHub Pages workflow on the v5 branch, a CNAME record for notes.ny2honolulu.com, and HTTPS.
Next in the series: Part 2: Privacy, Proof, and a Clean Commit.