Pre-built output is now committed to the repository so that Quartz can skip the build step during plugin installation. The prepare script is removed to prevent redundant builds when installing from npm/git. |
||
|---|---|---|
| .github/workflows | ||
| dist | ||
| src | ||
| test | ||
| types | ||
| .eslintrc.json | ||
| .gitignore | ||
| .prettierrc | ||
| CHANGELOG.md | ||
| LICENSE | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| tsup.config.ts | ||
| vitest.config.ts | ||
Quartz Community Plugin Template
Production-ready template for building, testing, and publishing Quartz community plugins. It mirrors
Quartz's native plugin patterns and uses a factory-function API similar to Astro integrations:
plugins are created by functions that return objects with name and lifecycle hooks.
Highlights
- ✅ Quartz-compatible transformer/filter/emitter examples
- ✅ TypeScript-first with exported types for consumers
- ✅
tsupbundling + declaration output - ✅ Vitest testing setup with example tests
- ✅ Linting/formatting with ESLint + Prettier
- ✅ CI workflow for checks and npm publishing
- ✅ Demonstrates CSS/JS resource injection and remark/rehype usage
Getting started
npm install
npm run build
Usage in Quartz
Install your plugin into a Quartz v5 site:
npx quartz plugin add github:quartz-community/plugin-template
Then register it in quartz.config.ts:
import * as ExternalPlugin from "./.quartz/plugins";
export default {
configuration: {
pageTitle: "My Garden",
},
plugins: {
transformers: [ExternalPlugin.ExampleTransformer({ highlightToken: "==" })],
filters: [ExternalPlugin.ExampleFilter({ allowDrafts: false })],
emitters: [ExternalPlugin.ExampleEmitter({ manifestSlug: "plugin-manifest" })],
},
externalPlugins: ["github:quartz-community/plugin-template"],
};
Plugin factory pattern (Astro-style)
Quartz plugins are factory functions that return an object with a name and hook implementations.
This mirrors Astro's integration pattern (a function returning an object of hooks), which makes
composition and configuration explicit and predictable.
import type { QuartzTransformerPlugin } from "@quartz-community/types";
export const MyTransformer: QuartzTransformerPlugin<{ enabled: boolean }> = (opts) => {
return {
name: "MyTransformer",
markdownPlugins() {
return [];
},
};
};
Examples included
Transformer
ExampleTransformer shows how to:
- apply a custom remark plugin
- run a rehype plugin
- inject CSS/JS resources
- perform a text transform hook
import { ExampleTransformer } from "@quartz-community/plugin-template";
ExampleTransformer({
highlightToken: "==",
headingClass: "example-plugin-heading",
enableGfm: true,
addHeadingSlugs: true,
});
The transformer uses a custom remark plugin to convert ==highlight== into bold text and a rehype
plugin to attach a class to all headings. It also injects a small inline CSS/JS snippet.
Filter
ExampleFilter demonstrates frontmatter-driven filtering:
ExampleFilter({
allowDrafts: false,
excludeTags: ["private", "wip"],
excludePathPrefixes: ["_drafts/", "_private/"],
});
Emitter
ExampleEmitter emits a JSON manifest of all pages:
ExampleEmitter({
manifestSlug: "plugin-manifest",
includeFrontmatter: true,
metadata: { project: "My Garden" },
transformManifest: (json) => json.replace("My Garden", "Quartz"),
});
API reference
ExampleTransformer(options)
| Option | Type | Default | Description |
|---|---|---|---|
highlightToken |
string |
"==" |
Token used to highlight text. |
headingClass |
string |
"example-plugin-heading" |
Class added to headings. |
enableGfm |
boolean |
true |
Enables remark-gfm. |
addHeadingSlugs |
boolean |
true |
Enables rehype-slug. |
ExampleFilter(options)
| Option | Type | Default | Description |
|---|---|---|---|
allowDrafts |
boolean |
false |
Publish draft pages. |
excludeTags |
string[] |
["private"] |
Tags to exclude. |
excludePathPrefixes |
string[] |
["_drafts/", "_private/"] |
Path prefixes to exclude. |
ExampleEmitter(options)
| Option | Type | Default | Description |
|---|---|---|---|
manifestSlug |
string |
"plugin-manifest" |
Output filename (without extension). |
includeFrontmatter |
boolean |
true |
Include frontmatter in output. |
metadata |
Record<string, unknown> |
{ generator: "Quartz Plugin Template" } |
Extra metadata in manifest. |
transformManifest |
(json: string) => string |
undefined |
Custom transformer for emitted JSON. |
manifestScriptClass |
string |
undefined |
Optional CSS class if rendered into HTML. |
Testing
npm test
Build and lint
npm run build
npm run lint
npm run format
Publishing
Tags matching v* trigger the GitHub Actions publish workflow. Ensure NPM_TOKEN is set in the
repository secrets.
Component Plugins (UI Components)
In addition to transformer/filter/emitter plugins, you can create component plugins that provide
UI elements for Quartz layouts. See src/components/ExampleComponent.tsx for a reference.
Component Pattern
import type { QuartzComponent, QuartzComponentConstructor } from "@quartz-community/types";
import style from "./styles/example.scss";
import script from "./scripts/example.inline.ts";
export default ((opts?: MyComponentOptions) => {
const Component: QuartzComponent = (props) => {
return <div class="my-component">...</div>;
};
Component.css = style;
Component.afterDOMLoaded = script;
return Component;
}) satisfies QuartzComponentConstructor;
Client-Side Scripts
Component scripts run in the browser and must handle Quartz's SPA navigation. Key patterns:
- Use
@ts-nocheck- Client scripts run in a different context than build-time code - Listen to
navevent - Fires after each page navigation (including initial load) - Listen to
prenavevent - Fires before navigation, use for saving state - Use
window.addCleanup()- Register cleanup functions for event listeners - Fetch
/static/contentIndex.json- Access page metadata for search, graph, etc.
See src/components/scripts/example.inline.ts for a complete example with all patterns.
Common Helper Functions
These utilities are commonly needed in component plugins:
function removeAllChildren(element) {
while (element.firstChild) element.removeChild(element.firstChild);
}
function simplifySlug(slug) {
return slug.endsWith("/index") ? slug.slice(0, -6) : slug;
}
function getCurrentSlug() {
let slug = window.location.pathname;
if (slug.startsWith("/")) slug = slug.slice(1);
if (slug.endsWith("/")) slug = slug.slice(0, -1);
return slug || "index";
}
State Persistence
Use localStorage for persistent state (survives browser close) and sessionStorage for
temporary state (like scroll positions):
localStorage.setItem("myPlugin-state", JSON.stringify(state));
sessionStorage.setItem("myPlugin-scrollTop", element.scrollTop.toString());
Migration Guide (from Quartz v4)
When migrating a v4 component to a standalone plugin:
- Replace Quartz imports with
@quartz-community/types - Copy utility functions (path helpers, DOM utils) into your plugin
- Use
@ts-nocheckfor inline scripts that can't be type-checked - Fetch data directly from
/static/contentIndex.jsoninstead of usingfetchData - Handle both data formats:
data.content || datafor contentIndex compatibility - Test with both local and production builds
License
MIT