Skip to content

fix: seed the graph from the slug stamped on <body>, not the URL - #15

Open
thoreplass wants to merge 2 commits into
quartz-community:mainfrom
thoreplass:fix-seed-graph-from-body-slug
Open

fix: seed the graph from the slug stamped on <body>, not the URL#15
thoreplass wants to merge 2 commits into
quartz-community:mainfrom
thoreplass:fix-seed-graph-from-body-slug

Conversation

@thoreplass

Copy link
Copy Markdown

The bug

Graph View renders a single unconnected dot on every page of a site whose slugs contain an uppercase or non-ASCII character. The panel mounts, the canvas is created, and contentIndex.json loads correctly - the search is simply seeded with a node that is not in the graph.

renderLocal() and showGlobalGraph() derived the current node from location.pathname, and the URL is not authoritative for this:

  • A host may normalise the case of pretty URLs. Netlify lowercases them, so a note published at Notes/Prices is served at /notes/prices while the content index is still keyed by Notes/Prices.
  • location.pathname is always percent-encoded. A slug containing non-ASCII characters never matches its own key either, even when the case is right.

The breadth-first search then starts from a slug that has no edges, returns only itself, and the panel draws one dot. Measured on a live Quartz 5 site (385 notes, 1304 links) served from Netlify:

source value present in contentIndex.json
document.body.dataset.slug Sider/Priser yes
location.pathname -> derived slug sider/priser no

383 of the 385 slugs on that site have an uppercase character, so effectively every page was affected, the landing page included.

The fix

Quartz already stamps the canonical slug on <body data-slug>, and it is the same value the router hands plugins in the nav event. Read it from there, and keep the URL as a fallback for pages that carry no data-slug.

getCurrentSlug() moves into src/util/slug.ts so the resolution can be unit-tested on its own - graph.inline.ts is @ts-nocheck'd and only reachable as a bundled string, so the previous getSlugFromUrl() had no way to be covered. tsup's inline-script loader bundles with resolveDir set to the script's directory, so the relative import is inlined into the emitted script exactly as the @quartz-community/utils imports already are; verified in dist/.

Tests

test/slug.test.ts covers four cases, and the first two fail on main:

  • uses the slug on <body> when the URL case differs
  • keeps a non-ASCII slug intact where the URL is percent-encoded
  • falls back to the URL when <body> carries no slug
  • strips the base path from that fallback

npm run typecheck, npm run lint and vitest run (8 tests) all pass, and npm run build succeeds.

🤖 Generated with Claude Code

thoreplass and others added 2 commits September 3, 2026 08:57
renderLocal() and showGlobalGraph() derived the current node from
location.pathname. The URL is not authoritative for this:

  - a host may normalise the case of pretty URLs. Netlify lowercases
    them, so a note published at Notes/Prices is served at /notes/prices
    while the content index is still keyed by Notes/Prices.
  - location.pathname is always percent-encoded, so a slug with
    non-ASCII characters never matches its own key either.

In both cases the breadth-first search was seeded with a node that is not
in the graph, so it returned only itself and the panel rendered a single
unconnected dot. Every page on such a site is affected, including the
landing page.

Quartz already stamps the canonical slug on <body data-slug> and hands it
to plugins in the nav event, so read it from there and keep the URL as a
fallback for pages that carry no data-slug. getCurrentSlug() moves to
src/util/slug.ts so the resolution can be unit-tested on its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An attribute that is present but empty must be treated as missing, not
returned as the current slug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant