Repository.discover(ctx.argv.directory) follows the symlink, so the repository it finds is the one
containing the link target. The file path is then made relative to that repository's workdir, but
data.filePath is relative to the project directory, so path.relative() resolves it against
process.cwd() and produces a path that points back out of the repository. Every lookup throws, and
the build prints the "isn't yet tracked by git" warning for every page that has no modified in its
frontmatter.
This only happens when content/ points outside the repository — a common setup for an Obsidian
vault kept in its own git repository. In an ordinary project content/ is inside the repo and the
arithmetic works out.
Reproduce
mkdir -p /tmp/vault && cd /tmp/vault && git init -q
printf -- '---\ntitle: A\n---\nhi\n' > a.md && git add -A && git commit -qm a
cd /path/to/a/quartz/project
rm -rf content && ln -s /tmp/vault content
npx quartz build
Warning: content/a.md isn't yet tracked by git, dates will be inaccurate
Measured
Version 0.1.0, @napi-rs/simple-git 0.1.22, node 26.5.1, macOS. Project at
~/Documents/Example, content → ~/Obsidian/QuartzProjekte/Example (its own git repository,
clean, 286 files tracked).
const repo = Repository.discover('content')
repo.workdir() // '/Users/boxi/Obsidian/QuartzProjekte/Example/' <- the vault, as expected
path.relative(repo.workdir(), 'content/en/formatting/special/escapes.md')
// '../../../Documents/Example/content/en/formatting/special/escapes.md' <- leads back out
await repo.getFileLatestModifiedDateAsync(that)
// throws: Failed to get commit for [...]
await repo.getFileLatestModifiedDateAsync('en/formatting/special/escapes.md')
// 1788573656000 <- the repository does know the file
250 of 254 pages warned. The four silent ones carry lastmod in their frontmatter, so modified ||=
short-circuits and git is never asked.
Suggested fix
Resolve the file path the same way discover() resolved the directory, so both sides of
path.relative live in the same tree:
-const relativePath = path.relative(repositoryWorkdir, fullFp)
+const relativePath = path.relative(repositoryWorkdir, fs.realpathSync(fullFp))
Measured with that one change applied to dist/index.js: the build finishes with 0 warnings, and
the rendered timestamp changes from 2026-09-05T03:41:43.937Z (milliseconds — a filesystem mtime,
the third priority silently taking over) to 2026-09-05T03:43:04.000Z (whole seconds — the commit).
fs is already imported. Behaviour for a non-symlinked content/ is unchanged, since realpathSync
is then a no-op apart from normalisation.
Two things worth deciding alongside it, both beyond what I measured:
realpathSync throws on a broken link, which would turn a dangling symlink into a hard failure
rather than a warning; wrapping it, or falling back to fullFp, keeps the current shape.
- The warning currently fires for every page on every build, which is 250 identical lines here. It
might read better as one line naming the count, or once per repository.
Repository.discover(ctx.argv.directory)follows the symlink, so the repository it finds is the onecontaining the link target. The file path is then made relative to that repository's workdir, but
data.filePathis relative to the project directory, sopath.relative()resolves it againstprocess.cwd()and produces a path that points back out of the repository. Every lookup throws, andthe build prints the "isn't yet tracked by git" warning for every page that has no
modifiedin itsfrontmatter.
This only happens when
content/points outside the repository — a common setup for an Obsidianvault kept in its own git repository. In an ordinary project
content/is inside the repo and thearithmetic works out.
Reproduce
Measured
Version 0.1.0,
@napi-rs/simple-git0.1.22, node 26.5.1, macOS. Project at~/Documents/Example,content→~/Obsidian/QuartzProjekte/Example(its own git repository,clean, 286 files tracked).
250 of 254 pages warned. The four silent ones carry
lastmodin their frontmatter, somodified ||=short-circuits and git is never asked.
Suggested fix
Resolve the file path the same way
discover()resolved the directory, so both sides ofpath.relativelive in the same tree:Measured with that one change applied to
dist/index.js: the build finishes with 0 warnings, andthe rendered timestamp changes from
2026-09-05T03:41:43.937Z(milliseconds — a filesystem mtime,the third priority silently taking over) to
2026-09-05T03:43:04.000Z(whole seconds — the commit).fsis already imported. Behaviour for a non-symlinkedcontent/is unchanged, sincerealpathSyncis then a no-op apart from normalisation.
Two things worth deciding alongside it, both beyond what I measured:
realpathSyncthrows on a broken link, which would turn a dangling symlink into a hard failurerather than a warning; wrapping it, or falling back to
fullFp, keeps the current shape.might read better as one line naming the count, or once per repository.