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.
|
||||
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
|
||||
|
||||
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)
|
||||
- **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.
|
||||
- **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
|
||||
|
||||
|
||||
@@ -109,6 +109,23 @@ Component.css = styles
|
||||
> [!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.
|
||||
|
||||
### 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
|
||||
|
||||
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
|
||||
|
||||
## 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 **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]
|
||||
> 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
|
||||
|
||||
```shell
|
||||
@@ -475,6 +528,108 @@ npm run build
|
||||
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
|
||||
|
||||
```shell
|
||||
|
||||
Reference in New Issue
Block a user