@kamal-hamza/quartz-plugin-pseudo
v0.3.6
Published
Pseudocode component plugin for Quartz v5 with KaTeX support.
Downloads
1,511
Maintainers
Readme
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 buildUsage in Quartz
Install 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"],
};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 testBuild and lint
npm run build
npm run lint
npm run formatPublishing
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;Receiving YAML Options in Component-Only Plugins
Processing plugins (transformers, filters, emitters, page types) receive options automatically
through their factory function. Component-only plugins (those with "category": ["component"])
are loaded via side-effect import and need an extra step to receive YAML options.
Export an init function from your plugin's entry point. Quartz's config-loader will call it with
the merged options from package.json defaultOptions and the user's quartz.config.yaml:
// src/index.ts
export function init(options?: Record<string, unknown>): void {
// Use the options to configure your plugin
const myOption = (options?.myOption as boolean) ?? false;
// e.g. register a view, set global state, etc.
}Then declare default values in your package.json manifest:
{
"quartz": {
"category": ["component"],
"defaultOptions": {
"myOption": false
}
}
}Users configure options in quartz.config.yaml:
plugins:
- source: github:your-username/my-component-plugin
enabled: true
options:
myOption: trueQuartz merges defaultOptions with the user's options (user values take precedence) and passes
the result to init(). If no init export exists, the plugin is loaded via side-effect import as
before — no breaking change for existing plugins.
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 - Use
fetchDataglobal - Access page metadata via thefetchDatapromise (handles base path correctly)
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 - Use the
fetchDataglobal to accesscontentIndex.jsonwith the correct base path - Test with both local and production builds
License
MIT
