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.
- ✅ 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
npm install
npm run buildInstall your plugin into a Quartz v5 site:
npx quartz plugin add github:quartz-community/plugin-templateThen 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"],
};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 [];
},
};
};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.
ExampleFilter demonstrates frontmatter-driven filtering:
ExampleFilter({
allowDrafts: false,
excludeTags: ["private", "wip"],
excludePathPrefixes: ["_drafts/", "_private/"],
});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"),
});| 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. |
| Option | Type | Default | Description |
|---|---|---|---|
allowDrafts |
boolean |
false |
Publish draft pages. |
excludeTags |
string[] |
["private"] |
Tags to exclude. |
excludePathPrefixes |
string[] |
["_drafts/", "_private/"] |
Path prefixes to exclude. |
| 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. |
npm testnpm run build
npm run lint
npm run formatTags matching v* trigger the GitHub Actions publish workflow. Ensure NPM_TOKEN is set in the
repository secrets.
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.
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;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.
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";
}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());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
MIT