@eclecticpassions/quartz-image-to-figure-caption
v0.2.1
Published
Convert Markdown image syntax to HTML figure with figcaption support, and add image size attributes.
Maintainers
Readme
Markdown Image Syntax Support for Quartz
This Quartz plugin adds the following features:
- Use Markdown image syntax to generate
<figure>and<figcaption>for HTML output - Differentiates between alt text and caption text as separate entities
- Supports Markdown link syntax inside caption text
Usage Examples
Add figure caption to an image:
HTML output:
<figure class="rehype-figure-title"> <img src="/path/to/image.webp" alt="Alt text here." loading="lazy"/> <figcaption> <p>Caption text here</p> </figcaption> </figure>Add figure caption that contains Markdown link:
")HTML output:
<figure class="rehype-figure-title"> <img src="/path/to/image.webp" alt="Alt text here." loading="lazy"/> <figcaption> <p>Caption text here but this time <a href="https://example.com" target="_blank" rel="noreferrer noopener" class="external external-link">contains a link</a></p> </figcaption> </figure>You can then style the
<figure>and<figcaption>. For example, inquartz/styles/custom.scss:
figure {
text-align: center;
img {
max-width: 100%;
height: auto;
margin: 0 auto;
}
figcaption * {
margin: 0;
font-size: 0.833rem;
color: var(--text-muted);
}
}Install
To install this plugin to your Quartz site, run:
npx quartz plugin install github:eclecticpassions/image-to-figure-captionTo update the plugin, run it with the
--latestflag like:npx quartz plugin install --latest github:eclecticpassions/image-to-figure-captionChange the
quartz.config.yamlfile to the following:- source: github:eclecticpassions/image-to-figure-caption enabled: trueRemove any addition config lines like
layout,position,priority, etc.
Disclaimer
[!WARNING] I used a few LLMs to help troubleshoot to get the
index.tsfile working. I am not a professional coder. Please review the code and packages before use. Check node package status by runningnpm auditregularly to check for vulnerabilities.
- The README and all documentation is written fully by me; only edits in the code are assisted by LLMs.
- This plugin uses the official Quartz-community plugin template
- Only Quartz v5.0.0 has been tested to work (if you run an older version of Quartz and want to test it, please open an issue, thanks!)
Dependencies
- futuraprime/rehype-figure-title
- mdast-util-from-markdown
- mdast-util-to-hast
- unist-util-visit
- @types/hast
Below is the README included in the Quartz plugin template.
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 - ✅ Pre-built
dist/ships in the repo — instant installation for users - ✅ 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[!important] After building, the
dist/directory should be committed to the repository. It is not gitignored, as Quartz uses it for pre-built distribution.
Build and Distribution
The template is configured to bundle all dependencies by default via noExternal: [/.*/] in tsup.config.ts. This ensures that users don't need to install any dependencies when using your plugin.
- Singleton Externals: Certain packages (
preact,vfile,unified,@jackyzha0/quartz) are kept external to ensure only one instance of them exists across all plugins. - Native Dependencies: If your plugin uses native dependencies (like
sharp,@napi-rs/simple-git, etc.), you must exclude them from bundling. Use a regex pattern innoExternalto exclude them, for example:noExternal: [/^(?!sharp)/]. - CI Verification: The included CI workflow verifies that
dist/is up to date on every push.
Usage in Quartz
Install your plugin into a Quartz v5 site:
npx quartz plugin add github:quartz-community/plugin-templateThen register it in quartz.config.yaml:
plugins:
- source: github:quartz-community/plugin-template
enabled: true
options:
highlightToken: "=="If you need to use the plugin in quartz.ts for advanced overrides:
import * as ExternalPlugin from "./.quartz/plugins";
export default {
plugins: {
transformers: [ExternalPlugin.ExampleTransformer({ highlightToken: "==" })],
},
};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
