Skip to content

fix(assets): only copy assets referenced by published pages (#2531) - #2544

Open
TJ2005 wants to merge 6 commits into
jackyzha0:v5from
TJ2005:fix/asset-filtering-2531
Open

TJ2005 wants to merge 6 commits into
jackyzha0:v5from
TJ2005:fix/asset-filtering-2531

Conversation

@TJ2005

@TJ2005 TJ2005 commented Sep 5, 2026

Copy link
Copy Markdown

Summary

Fixes #2531 — Assets from unpublished notes were being emitted when explicit-publish (and other filter plugins) are enabled.

Changes

1. New Config Option (quartz/cfg.ts)

Added publishAssets?: "all" | "referenced" to GlobalConfiguration:

  • "all" (default) — copies all non-markdown files (backwards compatible, preserves existing behavior)
  • "referenced" — only copies assets referenced by published pages

2. Assets Emitter Rewrite (quartz/plugins/emitters/assets.ts)

Modified emit() and partialEmit() to:

  1. Walk the filtered content's HAST trees and extract all src references (img, video, audio, iframe, source, track)
  2. Normalize paths relative to each markdown file's directory
  3. Only copy files that are both on disk AND referenced by published content

3. Test Coverage (quartz/plugins/emitters/assets.test.ts + fixture)

Added integration test with fixture that verifies:

  • Published page assets → copied ✓
  • Private page assets → NOT copied ✓
  • Draft page assets → NOT copied ✓
  • Orphan assets (unreferenced) → NOT copied ✓

Usage

In quartz.config.yaml:

configuration:
  publishAssets: "referenced"  # or "all" (default)
plugins:
  - source: "@quartz-community/explicit-publish"
    enabled: true
  # ... other plugins

Backwards Compatibility

  • Default "all" preserves existing behavior — existing sites unaffected
  • Opt-in "referenced" enables the new filtering

Testing

All 164 existing tests pass + new integration test.

- Add  config option ("all" | "referenced") to GlobalConfiguration
- Rewrite Assets emitter to extract asset refs from filtered content HAST
- Only copy intersected files when publishAssets: "referenced"
- Add integration test with fixture covering ExplicitPublish/RemoveDrafts

Fixes jackyzha0#2531
Copilot AI lite review requested due to automatic review settings September 5, 2026 08:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The referenced-asset extraction and incremental emit logic have correctness gaps, and the new integration test is currently non-portable/broken due to incorrect paths and a hard-coded Node binary.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds an opt-in publishAssets: "referenced" mode so the Assets emitter copies only assets referenced by published pages (to avoid leaking attachments from filtered/unpublished notes), and introduces a fixture-based integration test for the behavior.

Changes:

  • Added configuration.publishAssets?: "all" | "referenced" (defaulting to "all" for backwards compatibility).
  • Reworked Assets emitter to scan processed HAST trees for asset references and conditionally copy only those assets.
  • Added an integration test + fixture vault to verify that assets referenced only by private/draft pages (or orphaned) are not emitted.
File summaries
File Description
quartz/cfg.ts Adds the new publishAssets global config option.
quartz/plugins/emitters/assets.ts Implements referenced-only asset discovery and conditional copying.
quartz/plugins/emitters/assets.test.ts Adds an integration test that runs a build and asserts emitted assets.
test/fixtures/asset-filtering/quartz.config.yaml Fixture config enabling filter plugins + publishAssets: "referenced".
test/fixtures/asset-filtering/content/published.md Published page that references two assets.
test/fixtures/asset-filtering/content/private.md Unpublished page that references a “secret” asset.
test/fixtures/asset-filtering/content/draft.md Draft page that references a draft-only asset.
test/fixtures/asset-filtering/content/diagram.png Asset referenced by published content (should be copied).
test/fixtures/asset-filtering/content/assets/nested.png Nested asset referenced by published content (should be copied).
test/fixtures/asset-filtering/content/secret.png Asset referenced only by private content (should not be copied).
test/fixtures/asset-filtering/content/draft-img.png Asset referenced only by draft content (should not be copied).
test/fixtures/asset-filtering/content/orphan.png Unreferenced asset (should not be copied).
Review details
  • Files reviewed: 12/12 changed files
  • Comments generated: 7
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread quartz/plugins/emitters/assets.test.ts Outdated
Comment on lines +11 to +15
const PROJECT_ROOT = path.join(__dirname, "..", "..", "..")
const TEST_FIXTURE = path.join(__dirname, "fixtures", "asset-filtering")
const OUTPUT_DIR = path.join(PROJECT_ROOT, "test/fixtures/asset-filtering/public")
const TEST_CONFIG = path.join(PROJECT_ROOT, "quartz.config.test.yaml")

Comment thread quartz/plugins/emitters/assets.test.ts Outdated

function runQuartzBuild(args: string[]): Promise<{ code: number; stdout: string; stderr: string }> {
return new Promise((resolve) => {
const child = spawn("/opt/homebrew/bin/node", [
Comment thread quartz/cfg.ts
Comment on lines +84 to +89
/**
* Controls which assets are copied to public/
* - "all": Copy all non-markdown files (backwards compatible, default)
* - "referenced": Only copy assets referenced by published pages
*/
publishAssets?: "all" | "referenced"
Comment on lines +49 to +62
describe("Assets emitter with publishAssets config", () => {
before(async () => {
await cleanOutput()
// Use test config for this test suite
const testConfigContent = await fs.readFile(TEST_CONFIG, "utf-8")
await fs.writeFile(path.join(PROJECT_ROOT, "quartz.config.yaml"), testConfigContent)
})

after(async () => {
await cleanOutput()
// Restore original config (quartz.config.default.yaml)
const defaultConfigContent = await fs.readFile(path.join(PROJECT_ROOT, "quartz.config.default.yaml"), "utf-8")
await fs.writeFile(path.join(PROJECT_ROOT, "quartz.config.yaml"), defaultConfigContent)
})
Comment on lines +25 to +27
function normalizeAssetPath(raw: string): string {
return path.posix.normalize(raw.split("?")[0].split("#")[0]).replace(/^\.\//, "")
}
Comment on lines +35 to +46
visit(tree, "element", (node: Element) => {
const src = node.properties?.src
if (!src || typeof src !== "string") return
if (src.startsWith("http://") || src.startsWith("https://") || src.startsWith("data:")) return

let normalized = src.split("?")[0].split("#")[0]
if (baseDir && !normalized.startsWith("/")) {
normalized = path.posix.join(baseDir, normalized)
}
normalized = path.posix.normalize(normalized).replace(/^\.\//, "")
assets.add(normalized)
})
Comment thread quartz/plugins/emitters/assets.ts Outdated
Comment on lines 101 to 103
const referencedAssets = publishAssets === "referenced" ? extractAssetReferences(content) : null

for (const changeEvent of changeEvents) {
- Fix test paths and use process.execPath for portability
- Add publishAssets to JSON schema for validation
- Handle root-relative asset paths (strip leading /)
- Track href links in addition to src attributes
- Fix partialEmit to handle markdown changes (re-extract references)
- Make test hermetic with proper config save/restore
- Expand test fixture config with full plugin set for correct filter behavior

Fixes jackyzha0#2531
@TJ2005
TJ2005 marked this pull request as draft September 7, 2026 05:40
@github-actions

github-actions Bot commented Sep 7, 2026 •

Copy link
Copy Markdown
built with Refined Cloudflare Pages Action

⚡ Cloudflare Pages Deployment

Name Status Preview Last Commit
quartz ✅ Ready (View Log) Visit Preview 5939909

- Fix Windows test failure: fs.readdir(recursive) returns backslash
  paths on Windows, breaking forward-slash assertions. Normalize
  separators before comparing.
- Simplify test hermeticity (single before/after cleanup, no more
  fragile double-branch config verification/restoration logic).
- Fix dead-code branch in partialEmit: when a markdown file's publish
  status changes without the asset file itself changing, reconcile the
  full referenced-asset set instead of only looking at changeEvents so
  watch mode correctly adds/removes affected assets.
- Remove files that were unintentionally included in this PR and are
  unrelated to asset filtering: quartz.config.yaml (previously removed
  from the repo on purpose), quartz.config.test.yaml, the two
  docs/architecture/*.md files, and a stray .omo session artifact
  (now gitignored).
@TJ2005
TJ2005 marked this pull request as ready for review September 14, 2026 12:47
@TJ2005

TJ2005 commented Sep 14, 2026

Copy link
Copy Markdown
Author

Pushed a follow-up commit addressing the remaining issues from review and CI:

Windows CI fix (root cause of the failing build-and-test (windows-latest) job): fs.readdir(dir, { recursive: true }) returns platform-native separators, so on Windows the test got back content\published.html while asserting against content/published.html — the build was actually succeeding, the assertion was just platform-broken. Normalized separators before comparing instead of adding more delays/retries.

Test hermeticity: simplified the config swap/restore in before/after to a single clean path (removed the redundant double-branch "if markdown changed" logic that didn't change behavior, and the extra write-verification/delay hacks from the last two commits, which were treating a symptom rather than the cause).

partialEmit correctness gap (flagged by Copilot): when a markdown file's publish status changes (e.g. a note goes from draft to published) without the asset file itself changing, the emitter now reconciles the full referenced-asset set against disk instead of only inspecting changeEvents — so watch mode correctly starts/stops emitting the affected assets instead of leaving stale or missing files in public/.

Removed unrelated files that had snuck into the diff (visible via gh pr diff --name-only, unrelated to asset filtering):

  • quartz.config.yaml — this is a real regression: a maintainer intentionally deleted this exact file in af583c1 ("chore: remove dev-mode config"); this PR had re-added it.
  • quartz.config.test.yaml (redundant with the fixture's own config)
  • docs/architecture/plugin-authoring.md and docs/architecture/quartz-architecture-deep-dive.md
  • .omo/run-continuation/... (a local session artifact; now gitignored)

All 164 tests pass locally, and I verified the config-swap test cleans up correctly (leaves no quartz.config.yaml behind, matching pre-PR repo state). Moving out of draft since CI should now be green.

This branch was successfully deployed

1 active deployment
Branch Preview — 59399092 Deployed Sep 14, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Assets from unpublished notes are emitted when explicit-publish is enabled

2 participants