Conversation
`slugifyFilePath` (@quartz-community/utils) is called directly by the engine (parse.ts, build.ts, assets.ts) rather than through any transformer/filter/emitter hook, so there was previously no supported way for a plugin to change how file paths become slugs/URLs — e.g. to transliterate accented characters instead of leaving them to be percent-encoded, or to change how punctuation is handled. Add an optional `slugify` field to `QuartzTransformerPluginInstance`. It's resolved once per build (`resolveSlugify` in the new quartz/util/slugify.ts) and stored on `BuildCtx.slugify`, falling back to the built-in `slugifyFilePath` when no plugin defines one. Quartz throws a configuration error at build time if more than one enabled transformer plugin defines `slugify`, rather than silently picking one. Every internal call site now goes through `ctx.slugify` instead of importing `slugifyFilePath` directly: processors/parse.ts, both `ctx.allSlugs` computations in build.ts, and both call sites in plugins/emitters/assets.ts. worker.ts resolves its own copy per thread since functions can't cross the worker boundary (WorkerSerializableBuildCtx now also omits `slugify`). Documented in docs/advanced/making plugins.md and docs/advanced/paths.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
built with Refined Cloudflare Pages Action⚡ Cloudflare Pages Deployment
|
Same change as jackyzha0/quartz#2555 (upstream PR), applied here ahead of it landing. Adds an optional `slugify` field to `QuartzTransformerPluginInstance`, resolved once per build via `resolveSlugify` (quartz/util/slugify.ts) and stored on `BuildCtx.slugify`, falling back to the built-in `slugifyFilePath` when no plugin defines one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ream The default slugifier doesn't transliterate accented characters (they end up percent-encoded, e.g. %C3%A9) and leaves punctuation like backticks, apostrophes and commas in the slug as-is, which several editors don't reliably auto-link -- see the example that prompted this. There's no supported hook for it yet (see jackyzha0/quartz#2555, opened to add one), and the responsible function is duplicated across ~80 files: the root node_modules copy of @quartz-community/utils, a separate copy each community plugin installs into its own node_modules, and several plugins (crawl-links in particular, which resolves [[wikilinks]] to hrefs) inline their own copy directly into their dist bundle instead of importing the package at runtime. scripts/patch-slugify.mjs finds every one of those copies by content (the function's distinctive body, not a fixed path, since parameter names and bundling vary) and patches `slugifyPath` in place to transliterate accents and turn risky punctuation into a separator, while deliberately leaving `slugifyPathPreserveCase` untouched -- the alias-redirects plugin's case-preserving-redirect feature compares that unpatched slug against the new clean canonical one and auto-emits a redirect page whenever they differ, so every old URL gets a redirect to its new clean URL for free, without touching any content file (only active on a case-sensitive filesystem, i.e. the Linux deploy target, not this Windows checkout). Wired into package.json's postinstall/install-plugins scripts for local convenience, and into quartz/cli/handlers.js's handleBuild -- the one call path guaranteed to run before any plugin module is loaded regardless of which command installed it (`npx quartz plugin install`, used by the Dockerfile, doesn't go through those npm scripts). Verified by reverting three of the patched files and rebuilding with no manual step: the hook re-patched them before the build ran. Once jackyzha0/quartz#2555 lands and this site's quartz/ is updated from upstream, this can be replaced with a small transformer plugin using the new `slugify` hook instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Does this PR also handle all edge cases? E.g. 404 handling, old path redirect, Bases data, Canvas data, etc. Not sure if this is the place to address this issue. |
There was a problem hiding this comment.
🟡 Changes recommended
Loader validation must accept slugify-only transformer plugins before the documented feature works.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds transformer-plugin slugification overrides across builds, parsing, assets, workers, and documentation.
Changes:
- Resolves one custom slugifier per build with conflict detection.
- Routes slug generation through
BuildCtx.slugify, including workers. - Adds tests and documents the new plugin API.
Review finding: slugify-only transformer plugins are rejected by loader validation (moderate, 3 votes), so the documented use case currently does not work.
File summaries
| File | Reviewed change |
|---|---|
quartz/worker.ts |
Re-resolves slugification per worker. |
quartz/util/slugify.ts |
Resolves custom or built-in slugifiers. |
quartz/util/slugify.test.ts |
Tests resolver behavior. |
quartz/util/ctx.ts |
Adds slugification to build context. |
quartz/processors/parse.ts |
Uses contextual slugification. |
quartz/plugins/types.ts |
Defines the new transformer hook. |
quartz/plugins/emitters/assets.ts |
Applies custom slugs to assets. |
quartz/build.ts |
Integrates custom slugification into builds. |
docs/advanced/paths.md |
Documents slug customization. |
docs/advanced/making plugins.md |
Documents the transformer API. |
Review details
- Files reviewed: 10/10 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
A plugin whose only transformer hook is `slugify` -- the common case, since it's usually the sole reason to write a slugify-overriding plugin -- was rejected by the loader's category detection: both places that check whether an already-instantiated plugin "looks like" a transformer (validateCategory, used when a plugin declares its category and gets checked post-instantiation, and detectCategoryFromModule, the auto-detection fallback when it doesn't) only tested for textTransform/markdownPlugins/htmlPlugins. Add `slugify` to both, in config-loader.ts. (quartz/plugins/loader/index.ts has a similarly-shaped check, but that file's `resolvePlugins`/`instantiatePlugin` have no callers anywhere in the actual build/CLI pipeline -- config loading goes through config-loader.ts's loadQuartzConfig() exclusively -- so touching it wouldn't do anything, and could misleadingly suggest it was part of the fix.) (Found by the Copilot review on the PR.) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
f355450 to
18f9115
Compare
Hi, About Bases data, Canvas data, etc. there is an issue. This PR wires ctx.slugify through every call site in quartz/ core, but @quartz-community/utils's own transformInternalLink/slugifyFilePath (used by crawl-links to resolve [[wikilinks]] to hrefs, and potentially by other community plugins that reference file paths. I haven't audited bases-page/canvas-page specifically) are not pluggable and call the stock algorithm internally. So a site adopting a slugify override gets correct page slugs, but any plugin that independently resolves paths via those utils functions would still resolve against the stock slugs :( . I hit this with crawl-links while building this (had to patch its bundled copy directly as a workaround on my own site, outside this PR). Fixing that fully needs a companion change in @quartz-community/utils to make its internal slugification injectable too. |
|
It feels like you had an issue with the created slugs and opted to make it a new plugin category. I feel like it should instead be either fixed or made a configuration option. I'll have to check the scope and cascading issues. |
Summary
slugifyfield toQuartzTransformerPluginInstance, letting a single transformer plugin override how file paths become slugs (and thus URLs). Today the only way to change this is to patch the internals of@quartz-community/utils'sslugifyPath, which the engine calls directly rather than through any transformer/filter/emitter hook — not just unsupported, but genuinely hard to do reliably, since that function ends up inlined into several plugins' own bundles as well as re-exported from the package itself.resolveSlugify(newquartz/util/slugify.ts) and stored onBuildCtx.slugify; falls back to the built-inslugifyFilePathwhen no plugin defines one. Throws a clear configuration error if more than one enabled transformer plugin definesslugify, rather than silently picking one.ctx.slugifyinstead of importingslugifyFilePathdirectly:processors/parse.ts, bothctx.allSlugscomputations inbuild.ts, and both call sites inplugins/emitters/assets.ts.worker.tsresolves its own copy per thread since functions can't cross the worker boundary (WorkerSerializableBuildCtxnow also omitsslugify).docs/advanced/making plugins.mdanddocs/advanced/paths.md.Motivation
The default slugifier doesn't transliterate accented characters (they end up percent-encoded in the URL) and leaves punctuation like backticks, apostrophes, and commas in the slug as-is — which trips up some tools/editors that try to auto-link the URL, and generally isn't what every site wants. There's currently no supported way to customize this from a plugin.
Test plan
npm test(167 tests, including newquartz/util/slugify.test.tscovering resolution + the multiple-override error)npx tsc --noEmitnpx prettier . --checkon touched filesslugifyis correctly picked up byprocessors/parse.tsand produces the expected slugnpx quartz buildandnpx quartz build --concurrency 2(exercises the worker-thread ctx reconstruction path) both produce correct output🤖 Generated with Claude Code