docs: add package layering, plugin type guide, i18n guide, bases views, and import reference
This commit is contained in:
@@ -50,6 +50,26 @@ This question is best answered by tracing what happens when a user (you!) runs `
|
|||||||
2. If it's not, we wire up the `"nav"` event to just be fired a single time after page load to allow for consistency across how state is setup across both SPA and non-SPA contexts.
|
2. If it's not, we wire up the `"nav"` event to just be fired a single time after page load to allow for consistency across how state is setup across both SPA and non-SPA contexts.
|
||||||
3. A separate `"render"` event can be dispatched when the DOM is updated in-place without a full navigation (e.g. after content decryption). Components that attach listeners to content elements should listen for both `"nav"` and `"render"`.
|
3. A separate `"render"` event can be dispatched when the DOM is updated in-place without a full navigation (e.g. after content decryption). Components that attach listeners to content elements should listen for both `"nav"` and `"render"`.
|
||||||
|
|
||||||
|
## Community Package Layering
|
||||||
|
|
||||||
|
Quartz v5 separates shared code into three community packages, each with a distinct responsibility:
|
||||||
|
|
||||||
|
- **`@quartz-community/types`** — Type definitions, interfaces, and the canonical `vfile` DataMap augmentation. This is the "contract" between Quartz and plugins. It has no runtime dependencies.
|
||||||
|
- **`@quartz-community/utils`** — Shared utility functions (path manipulation, DOM helpers, sorting, date formatting, JSX conversion, etc.). Depends on `@quartz-community/types`.
|
||||||
|
- **`@quartz-community/runtime`** — Browser-only utilities for client-side scripts (event handling, navigation, storage, script loading). Depends on both `types` and `utils`.
|
||||||
|
|
||||||
|
```
|
||||||
|
types (no deps)
|
||||||
|
↑
|
||||||
|
utils (depends on types)
|
||||||
|
↑
|
||||||
|
runtime (depends on types + utils)
|
||||||
|
↑
|
||||||
|
plugins (depend on any combination)
|
||||||
|
```
|
||||||
|
|
||||||
|
Plugins should import types from `@quartz-community/types`, utility functions from `@quartz-community/utils`, and browser utilities from `@quartz-community/runtime`. This layering ensures plugins don't depend on Quartz core.
|
||||||
|
|
||||||
## Plugin System
|
## Plugin System
|
||||||
|
|
||||||
Page types define how a category of pages is rendered. They are configured in the `pageTypes` array in `quartz.config.yaml`.
|
Page types define how a category of pages is rendered. They are configured in the `pageTypes` array in `quartz.config.yaml`.
|
||||||
@@ -64,6 +84,9 @@ There are now four plugin categories:
|
|||||||
- **Filters**: Filter content (remove drafts, explicit publish)
|
- **Filters**: Filter content (remove drafts, explicit publish)
|
||||||
- **Emitters**: Reduce over content (generate RSS, sitemaps, alias redirects, OG images)
|
- **Emitters**: Reduce over content (generate RSS, sitemaps, alias redirects, OG images)
|
||||||
- **Page Types**: Define how pages are rendered. Each page type handles a specific kind of page (content notes, folder listings, tag listings, 404). The `PageTypeDispatcher` emitter routes pages to the appropriate page type plugin based on the content.
|
- **Page Types**: Define how pages are rendered. Each page type handles a specific kind of page (content notes, folder listings, tag listings, 404). The `PageTypeDispatcher` emitter routes pages to the appropriate page type plugin based on the content.
|
||||||
|
- **Bases Views**: Custom view renderers for the `bases-page` plugin's database-like view system. Plugins can register new view types (e.g., timeline, kanban) via the `ViewRegistry`. See [[making plugins#Bases Views]] for details.
|
||||||
|
|
||||||
|
Note that plugin types are **not mutually exclusive** — a single plugin can be a transformer AND provide components (e.g., `obsidian-flavored-markdown`), or be a page type AND provide custom frames (e.g., `canvas-page`).
|
||||||
|
|
||||||
### Plugin Resolution
|
### Plugin Resolution
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,23 @@ Component.css = styles
|
|||||||
> [!warning]
|
> [!warning]
|
||||||
> Quartz does not use CSS modules so any styles you declare here apply _globally_. If you only want it to apply to your component, make sure you use specific class names and selectors.
|
> Quartz does not use CSS modules so any styles you declare here apply _globally_. If you only want it to apply to your component, make sure you use specific class names and selectors.
|
||||||
|
|
||||||
|
### Internationalization
|
||||||
|
|
||||||
|
Component plugins should use the i18n pattern for any user-facing strings. See [[making plugins#Internationalization (i18n)]] for the full setup guide.
|
||||||
|
|
||||||
|
Quick reference:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { i18n } from "../i18n"
|
||||||
|
|
||||||
|
const MyComponent: QuartzComponent = ({ cfg }) => {
|
||||||
|
const t = i18n(cfg.locale ?? "en-US").components.myComponent
|
||||||
|
return <h2>{t.title}</h2>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Always provide at least an `en-US` locale as the fallback. Additional locales are optional but encouraged for international reach.
|
||||||
|
|
||||||
### Scripts and Interactivity
|
### Scripts and Interactivity
|
||||||
|
|
||||||
For interactivity, you can declare `.beforeDOMLoaded` and `.afterDOMLoaded` properties on the component. These should be strings containing the JavaScript to be executed in the browser.
|
For interactivity, you can declare `.beforeDOMLoaded` and `.afterDOMLoaded` properties on the component. These should be strings containing the JavaScript to be executed in the browser.
|
||||||
|
|||||||
@@ -63,6 +63,25 @@ The plugin's `package.json` should declare dependencies on `@quartz-community/ty
|
|||||||
|
|
||||||
## Plugin Types
|
## Plugin Types
|
||||||
|
|
||||||
|
## Choosing a Plugin Type
|
||||||
|
|
||||||
|
Quartz supports six plugin capabilities. A single plugin can combine multiple types.
|
||||||
|
|
||||||
|
| I want to... | Plugin Type |
|
||||||
|
| ------------------------------------------------ | ----------- |
|
||||||
|
| Transform Markdown/HTML content | Transformer |
|
||||||
|
| Decide which pages to publish | Filter |
|
||||||
|
| Generate output files (RSS, sitemaps, manifests) | Emitter |
|
||||||
|
| Define how a category of pages renders | Page Type |
|
||||||
|
| Add a UI component to the layout | Component |
|
||||||
|
| Add a custom view to the Bases database system | Bases View |
|
||||||
|
|
||||||
|
These are **not mutually exclusive**. For example:
|
||||||
|
|
||||||
|
- `obsidian-flavored-markdown` is both a **transformer** (processes OFM syntax) and provides **components** (mermaid rendering)
|
||||||
|
- `canvas-page` is a **page type** that also provides a custom **frame**
|
||||||
|
- A plugin could be a **transformer** that adds metadata AND a **component** that displays it
|
||||||
|
|
||||||
### Transformers
|
### Transformers
|
||||||
|
|
||||||
Transformers **map** over content, taking a Markdown file and outputting modified content or adding metadata to the file itself.
|
Transformers **map** over content, taking a Markdown file and outputting modified content or adding metadata to the file itself.
|
||||||
@@ -466,6 +485,40 @@ When a user installs your plugin, Quartz automatically loads the frame from the
|
|||||||
> [!tip]
|
> [!tip]
|
||||||
> See the [`canvas-page`](https://github.com/quartz-community/canvas-page) plugin for a complete real-world example of a plugin-provided frame.
|
> See the [`canvas-page`](https://github.com/quartz-community/canvas-page) plugin for a complete real-world example of a plugin-provided frame.
|
||||||
|
|
||||||
|
### Bases Views
|
||||||
|
|
||||||
|
The `bases-page` plugin provides a database-like view system similar to Obsidian Bases. Other plugins can register custom view types via the `ViewRegistry`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { viewRegistry } from "@quartz-community/bases-page";
|
||||||
|
import type { ViewTypeRegistration } from "@quartz-community/bases-page";
|
||||||
|
|
||||||
|
viewRegistry.register({
|
||||||
|
id: "timeline",
|
||||||
|
name: "Timeline",
|
||||||
|
icon: "git-branch",
|
||||||
|
render: ({ entries, view, slug, allSlugs }) => (
|
||||||
|
<div class="bases-timeline">
|
||||||
|
{entries.map(entry => <div>{entry.properties.title}</div>)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
css: `.bases-timeline { display: flex; flex-direction: column; }`,
|
||||||
|
afterDOMLoaded: `document.addEventListener("nav", () => { /* setup */ })`,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Each view registration includes:
|
||||||
|
|
||||||
|
- `id`: Unique identifier (e.g., `"timeline"`, `"kanban"`)
|
||||||
|
- `name`: Display name shown in the view selector
|
||||||
|
- `icon`: Optional Lucide icon name
|
||||||
|
- `render`: Function that receives `ViewRendererProps` and returns Preact JSX
|
||||||
|
- `css`: Optional CSS string (deduplicated by view ID)
|
||||||
|
- `afterDOMLoaded`: Optional client-side script (same lifecycle as component scripts)
|
||||||
|
- `options`: Optional configuration passed to every render invocation
|
||||||
|
|
||||||
|
The `ViewRegistry` is a global singleton (via `Symbol.for`) ensuring all copies of the module share the same registry.
|
||||||
|
|
||||||
## Building and Testing
|
## Building and Testing
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
@@ -475,6 +528,108 @@ npm run build
|
|||||||
npx tsup
|
npx tsup
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## What to Import from Where
|
||||||
|
|
||||||
|
| You need... | Import from |
|
||||||
|
| ---------------------------------------------------------------------- | ----------------------------------------------------------------- |
|
||||||
|
| Type definitions (`QuartzTransformerPlugin`, `QuartzComponent`, etc.) | `@quartz-community/types` |
|
||||||
|
| Path utilities (`simplifySlug`, `resolveRelative`, `pathToRoot`) | `@quartz-community/utils/path` |
|
||||||
|
| DOM utilities (`removeAllChildren`, `registerEscapeHandler`) | `@quartz-community/utils/dom` |
|
||||||
|
| JSX conversion (`htmlToJsx`) | `@quartz-community/utils/jsx` |
|
||||||
|
| Language utilities (`classNames`, `capitalize`) | `@quartz-community/utils/lang` |
|
||||||
|
| Date/sort utilities (`formatDate`, `getDate`, `byDateAndAlphabetical`) | `@quartz-community/utils/date` and `@quartz-community/utils/sort` |
|
||||||
|
| HTML escaping (`escapeHTML`, `unescapeHTML`) | `@quartz-community/utils/escape` |
|
||||||
|
| Emoji utilities (`getIconCode`) | `@quartz-community/utils/emoji` |
|
||||||
|
| Browser runtime (`onNav`, `onRender`, `fetchContentIndex`) | `@quartz-community/runtime` |
|
||||||
|
|
||||||
|
Do **not** import from `@jackyzha0/quartz` or from `vfile` directly. Use the community packages instead.
|
||||||
|
|
||||||
|
## Internationalization (i18n)
|
||||||
|
|
||||||
|
Plugins should provide their own translations for user-facing strings. Do **not** hardcode strings in components.
|
||||||
|
|
||||||
|
### Setting Up i18n
|
||||||
|
|
||||||
|
Create the following structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/i18n/
|
||||||
|
├── index.ts
|
||||||
|
└── locales/
|
||||||
|
└── en-US.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
**`src/i18n/locales/en-US.ts`** (required base locale):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
myPlugin: {
|
||||||
|
title: "My Plugin",
|
||||||
|
description: "A description",
|
||||||
|
itemCount: ({ count }: { count: number }) => (count === 1 ? "1 item" : `${count} items`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**`src/i18n/index.ts`**:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import enUS from "./locales/en-US"
|
||||||
|
|
||||||
|
const locales: Record<string, typeof enUS> = {
|
||||||
|
"en-US": enUS,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function i18n(locale: string) {
|
||||||
|
return locales[locale] || enUS
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using i18n in Components
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { i18n } from "../i18n"
|
||||||
|
|
||||||
|
const MyComponent: QuartzComponent = ({ cfg }) => {
|
||||||
|
const locale = cfg.locale ?? "en-US"
|
||||||
|
const t = i18n(locale).components.myPlugin
|
||||||
|
return <h2>{t.title}</h2>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding Translations
|
||||||
|
|
||||||
|
To add a new locale, copy `en-US.ts`, translate the strings, and register it:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/i18n/locales/fr-FR.ts
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
myPlugin: {
|
||||||
|
title: "Mon Plugin",
|
||||||
|
description: "Une description",
|
||||||
|
itemCount: ({ count }: { count: number }) =>
|
||||||
|
count === 1 ? "1 élément" : `${count} éléments`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/i18n/index.ts
|
||||||
|
import enUS from "./locales/en-US"
|
||||||
|
import frFR from "./locales/fr-FR"
|
||||||
|
|
||||||
|
const locales: Record<string, typeof enUS> = {
|
||||||
|
"en-US": enUS,
|
||||||
|
"fr-FR": frFR,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use [BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag) locale codes (e.g., `en-US`, `de-DE`, `ja-JP`, `zh-CN`). For dynamic content, use function-based translations as shown with `itemCount` above.
|
||||||
|
|
||||||
## Installing Your Plugin
|
## Installing Your Plugin
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
|||||||
Reference in New Issue
Block a user